/*
 * Copyright 2014 Google Inc. 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.openrtb.json;

import static com.google.openrtb.json.OpenRtbJsonUtils.endArray;
import static com.google.openrtb.json.OpenRtbJsonUtils.endObject;
import static com.google.openrtb.json.OpenRtbJsonUtils.getCurrentName;
import static com.google.openrtb.json.OpenRtbJsonUtils.startArray;
import static com.google.openrtb.json.OpenRtbJsonUtils.startObject;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.google.common.io.CharSource;
import com.google.common.io.Closeables;
import com.google.openrtb.OpenRtb.AdUnitId;
import com.google.openrtb.OpenRtb.ContextSubtype;
import com.google.openrtb.OpenRtb.ContextType;
import com.google.openrtb.OpenRtb.DataAssetType;
import com.google.openrtb.OpenRtb.EventTrackingMethod;
import com.google.openrtb.OpenRtb.EventType;
import com.google.openrtb.OpenRtb.ImageAssetType;
import com.google.openrtb.OpenRtb.LayoutId;
import com.google.openrtb.OpenRtb.NativeRequest;
import com.google.openrtb.OpenRtb.NativeResponse;
import com.google.openrtb.OpenRtb.PlacementType;
import com.google.openrtb.util.ProtoUtils;
import com.google.protobuf.ByteString;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;

/**
 * Desserializes OpenRTB {@link NativeRequest}/{@link NativeResponse} messages from JSON.
 *
 * <p>This class is threadsafe.
 */
public class OpenRtbNativeJsonReader extends AbstractOpenRtbJsonReader {
  private OpenRtbJsonReader coreReader;

  protected OpenRtbNativeJsonReader(OpenRtbJsonFactory factory) {
    super(factory);
  }

  /**
   * Desserializes a {@link NativeRequest} from a JSON string, provided as a {@link ByteString}.
   */
  public NativeRequest readNativeRequest(ByteString bs) throws IOException {
    return readNativeRequest(bs.newInput());
  }

  /**
   * Desserializes a {@link NativeRequest} from a JSON string, provided as a {@link CharSequence}.
   */
  public NativeRequest readNativeRequest(CharSequence chars) throws IOException {
    return readNativeRequest(CharSource.wrap(chars).openStream());
  }

  /**
   * Desserializes a {@link NativeRequest} from JSON, streamed from a {@link Reader}.
   */
  public NativeRequest readNativeRequest(Reader reader) throws IOException {
    return ProtoUtils.built(readNativeRequest(factory().getJsonFactory().createParser(reader)));
  }

  /**
   * Desserializes a {@link NativeRequest} from JSON, streamed from an {@link InputStream}.
   */
  public NativeRequest readNativeRequest(InputStream is) throws IOException {
    try {
      return ProtoUtils.built(readNativeRequest(factory().getJsonFactory().createParser(is)));
    } finally {
      Closeables.closeQuietly(is);
    }
  }

  /**
   * Desserializes a {@link NativeRequest} from JSON, with a provided {@link JsonParser}
   * which allows several choices of input and encoding.
   */
  public final NativeRequest.Builder readNativeRequest(JsonParser par) throws IOException {
    if (emptyToNull(par)) {
      return null;
    }
    NativeRequest.Builder req = NativeRequest.newBuilder();
    boolean rootNativeField = false;
    boolean firstField = true;
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        if (firstField) {
          firstField = false;
          if ((rootNativeField = "native".equals(fieldName)) == true) {
            startObject(par);
            fieldName = getCurrentName(par);
            par.nextToken();
          }
        }
        if (par.getCurrentToken() != JsonToken.VALUE_NULL) {
          readNativeRequestField(par, req, fieldName);
        }
      }
    }
    if (rootNativeField && !endObject(par)) {
      par.nextToken();
    }
    return req;
  }

  protected void readNativeRequestField(JsonParser par, NativeRequest.Builder req, String fieldName)
      throws IOException {
    switch (fieldName) {
      case "ver":
        req.setVer(par.getText());
        break;
      case "layout": {
          LayoutId value = LayoutId.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            req.setLayout(value);
          }
        }
        break;
      case "adunit": {
          AdUnitId value = AdUnitId.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            req.setAdunit(value);
          }
        }
        break;
      case "plcmtcnt":
        req.setPlcmtcnt(par.getIntValue());
        break;
      case "seq":
        req.setSeq(par.getIntValue());
        break;
      case "assets":
        for (startArray(par); endArray(par); par.nextToken()) {
          req.addAssets(readReqAsset(par));
        }
        break;
      case "context": {
          ContextType value = ContextType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            req.setContext(value);
          }
        }
        break;
      case "contextsubtype": {
          ContextSubtype value = ContextSubtype.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            req.setContextsubtype(value);
          }
        }
        break;
      case "plcmttype": {
          PlacementType value = PlacementType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            req.setPlcmttype(value);
          }
        }
        break;
      case "aurlsupport":
        req.setAurlsupport(par.getValueAsBoolean());
        break;
      case "durlsupport":
        req.setDurlsupport(par.getValueAsBoolean());
        break;
      case "privacy":
        req.setPrivacy(par.getValueAsBoolean());
        break;
      case "eventtrackers":
        for (startArray(par); endArray(par); par.nextToken()) {
          req.addEventtrackers(readReqEventTrackers(par));
        }
        break;
      default:
        readOther(req, par, fieldName);
    }
  }

  public final NativeRequest.Asset.Builder readReqAsset(JsonParser par) throws IOException {
    NativeRequest.Asset.Builder asset = NativeRequest.Asset.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readReqAssetField(par, asset, fieldName);
      }
    }
    return asset;
  }

  protected void readReqAssetField(
      JsonParser par, NativeRequest.Asset.Builder asset, String fieldName) throws IOException {
    switch (fieldName) {
      case "id":
        asset.setId(par.getIntValue());
        break;
      case "required":
        asset.setRequired(par.getValueAsBoolean());
        break;
      case "title":
        asset.setTitle(readReqTitle(par));
        break;
      case "img":
        asset.setImg(readReqImage(par));
        break;
      case "video":
        asset.setVideo(coreReader().readVideo(par));
        break;
      case "data":
        asset.setData(readReqData(par));
        break;
      default:
        readOther(asset, par, fieldName);
    }
  }

  public final NativeRequest.Asset.Title.Builder readReqTitle(JsonParser par)
      throws IOException {
    NativeRequest.Asset.Title.Builder title = NativeRequest.Asset.Title.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readReqTitleField(par, title, fieldName);
      }
    }
    return title;
  }

  protected void readReqTitleField(
      JsonParser par, NativeRequest.Asset.Title.Builder title, String fieldName)
      throws IOException {
    switch (fieldName) {
      case "len":
        title.setLen(par.getIntValue());
        break;
      default:
        readOther(title, par, fieldName);
    }
  }

  public final NativeRequest.Asset.Image.Builder readReqImage(JsonParser par)
      throws IOException {
    NativeRequest.Asset.Image.Builder req = NativeRequest.Asset.Image.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readReqImageField(par, req, fieldName);
      }
    }
    return req;
  }

  protected void readReqImageField(
      JsonParser par, NativeRequest.Asset.Image.Builder image, String fieldName)
      throws IOException {
    switch (fieldName) {
      case "type": {
          ImageAssetType value = ImageAssetType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            image.setType(value);
          }
        }
        break;
      case "w":
        image.setW(par.getIntValue());
        break;
      case "h":
        image.setH(par.getIntValue());
        break;
      case "wmin":
        image.setWmin(par.getIntValue());
        break;
      case "hmin":
        image.setHmin(par.getIntValue());
        break;
      case "mimes":
        for (startArray(par); endArray(par); par.nextToken()) {
          image.addMimes(par.getText());
        }
        break;
      default:
        readOther(image, par, fieldName);
    }
  }

  public final NativeRequest.Asset.Data.Builder readReqData(JsonParser par) throws IOException {
    NativeRequest.Asset.Data.Builder data = NativeRequest.Asset.Data.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readReqDataField(par, data, fieldName);
      }
    }
    return data;
  }

  protected void readReqDataField(
      JsonParser par, NativeRequest.Asset.Data.Builder data, String fieldName) throws IOException {
    switch (fieldName) {
      case "type": {
          DataAssetType value = DataAssetType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            data.setType(value);
          }
        }
        break;
      case "len":
        data.setLen(par.getIntValue());
        break;
      default:
        readOther(data, par, fieldName);
    }
  }

  public final NativeRequest.EventTrackers.Builder readReqEventTrackers(JsonParser par)
      throws IOException {
    NativeRequest.EventTrackers.Builder trackers = NativeRequest.EventTrackers.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readReqEventTrackersField(par, trackers, fieldName);
      }
    }
    return trackers;
  }

  protected void readReqEventTrackersField(
      JsonParser par, NativeRequest.EventTrackers.Builder trackers, String fieldName)
          throws IOException {
    switch (fieldName) {
      case "event": {
          EventType value = EventType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            trackers.setEvent(value);
          }
        }
        break;
      case "methods":
        for (startArray(par); endArray(par); par.nextToken()) {
          EventTrackingMethod value = EventTrackingMethod.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            trackers.addMethods(value);
          }
        }
        break;
      default:
        readOther(trackers, par, fieldName);
    }
  }

  /**
   * Desserializes a {@link NativeResponse} from a JSON string, provided as a {@link ByteString}.
   */
  public NativeResponse readNativeResponse(ByteString bs) throws IOException {
    return readNativeResponse(bs.newInput());
  }

  /**
   * Desserializes a {@link NativeResponse} from a JSON string, provided as a {@link CharSequence}.
   */
  public NativeResponse readNativeResponse(CharSequence chars) throws IOException {
    return readNativeResponse(CharSource.wrap(chars).openStream());
  }

  /**
   * Desserializes a {@link NativeResponse} from JSON, streamed from a {@link Reader}.
   */
  public NativeResponse readNativeResponse(Reader reader) throws IOException {
    return ProtoUtils.built(readNativeResponse(factory().getJsonFactory().createParser(reader)));
  }

  /**
   * Desserializes a {@link NativeResponse} from JSON, streamed from an {@link InputStream}.
   */
  public NativeResponse readNativeResponse(InputStream is) throws IOException {
    try {
      return ProtoUtils.built(readNativeResponse(factory().getJsonFactory().createParser(is)));
    } finally {
      Closeables.closeQuietly(is);
    }
  }

  /**
   * Desserializes a {@link NativeResponse} from JSON, with a provided {@link JsonParser}
   * which allows several choices of input and encoding.
   */
  public final NativeResponse.Builder readNativeResponse(JsonParser par) throws IOException {
    if (emptyToNull(par)) {
      return null;
    }
    NativeResponse.Builder resp = NativeResponse.newBuilder();
    boolean rootNativeField = false;
    boolean firstField = true;
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        if (firstField) {
          firstField = false;
          if ((rootNativeField = "native".equals(fieldName)) == true) {
            startObject(par);
            fieldName = getCurrentName(par);
            par.nextToken();
          }
        }
        if (par.getCurrentToken() != JsonToken.VALUE_NULL) {
          readNativeResponseField(par, resp, fieldName);
        }
      }
    }
    if (rootNativeField && !endObject(par)) {
      par.nextToken();
    }
    return resp;
  }

  protected void readNativeResponseField(
      JsonParser par, NativeResponse.Builder resp, String fieldName) throws IOException {
    switch (fieldName) {
      case "ver":
        resp.setVer(par.getText());
        break;
      case "assets":
        for (startArray(par); endArray(par); par.nextToken()) {
          resp.addAssets(readRespAsset(par));
        }
        break;
      case "link":
        resp.setLink(readRespLink(par));
        break;
      case "imptrackers":
        for (startArray(par); endArray(par); par.nextToken()) {
          resp.addImptrackers(par.getText());
        }
        break;
      case "jstracker":
        resp.setJstracker(par.getText());
        break;
      case "assetsurl":
        resp.setAssetsurl(par.getText());
        break;
      case "dcourl":
        resp.setDcourl(par.getText());
        break;
      case "eventtrackers":
        for (startArray(par); endArray(par); par.nextToken()) {
          resp.addEventtrackers(readRespEventTracker(par));
        }
        break;
      case "privacy":
        resp.setPrivacy(par.getText());
        break;
      default:
        readOther(resp, par, fieldName);
    }
  }

  public final NativeResponse.Asset.Builder readRespAsset(JsonParser par) throws IOException {
    NativeResponse.Asset.Builder asset = NativeResponse.Asset.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespAssetField(par, asset, fieldName);
      }
    }
    return asset;
  }

  protected void readRespAssetField(
      JsonParser par, NativeResponse.Asset.Builder asset, String fieldName) throws IOException {
    switch (fieldName) {
      case "id":
        asset.setId(par.getIntValue());
        break;
      case "required":
        asset.setRequired(par.getValueAsBoolean());
        break;
      case "title":
        asset.setTitle(readRespTitle(par));
        break;
      case "img":
        asset.setImg(readRespImage(par));
        break;
      case "video":
        asset.setVideo(readRespVideo(par));
        break;
      case "data":
        asset.setData(readRespData(par));
        break;
      case "link":
        asset.setLink(readRespLink(par));
        break;
      default:
        readOther(asset, par, fieldName);
    }
  }

  public final NativeResponse.Asset.Title.Builder readRespTitle(JsonParser par)
      throws IOException {
    NativeResponse.Asset.Title.Builder title = NativeResponse.Asset.Title.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespTitleField(par, title, fieldName);
      }
    }
    return title;
  }

  protected void readRespTitleField(
      JsonParser par, NativeResponse.Asset.Title.Builder title, String fieldName)
      throws IOException {
    switch (fieldName) {
      case "text":
        title.setText(par.getText());
        break;
      case "len":
        title.setLen(par.getIntValue());
        break;
      default:
        readOther(title, par, fieldName);
    }
  }

  public final NativeResponse.Asset.Image.Builder readRespImage(JsonParser par)
      throws IOException {
    NativeResponse.Asset.Image.Builder image = NativeResponse.Asset.Image.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespImageField(par, image, fieldName);
      }
    }
    return image;
  }

  protected void readRespImageField(
      JsonParser par, NativeResponse.Asset.Image.Builder image, String fieldName)
      throws IOException {
    switch (fieldName) {
      case "url":
        image.setUrl(par.getText());
        break;
      case "type": {
          ImageAssetType value = ImageAssetType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            image.setType(value);
          }
        }
        break;
      case "w":
        image.setW(par.getIntValue());
        break;
      case "h":
        image.setH(par.getIntValue());
        break;
      default:
        readOther(image, par, fieldName);
    }
  }

  public final NativeResponse.Asset.Video.Builder readRespVideo(JsonParser par)
      throws IOException {
    NativeResponse.Asset.Video.Builder video = NativeResponse.Asset.Video.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespVideoField(par, video, fieldName);
      }
    }
    return video;
  }

  protected void readRespVideoField(
      JsonParser par, NativeResponse.Asset.Video.Builder video, String fieldName)
      throws IOException {
    switch (fieldName) {
      case "vasttag":
        video.setVasttag(par.getText());
        break;
      default:
        readOther(video, par, fieldName);
    }
  }

  public final NativeResponse.Asset.Data.Builder readRespData(JsonParser par) throws IOException {
    NativeResponse.Asset.Data.Builder data = NativeResponse.Asset.Data.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespDataField(par, data, fieldName);
      }
    }
    return data;
  }

  protected void readRespDataField(
      JsonParser par, NativeResponse.Asset.Data.Builder data, String fieldName) throws IOException {
    switch (fieldName) {
      case "label":
        data.setLabel(par.getText());
        break;
      case "value":
        data.setValue(par.getText());
        break;
      case "type": {
          DataAssetType value = DataAssetType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            data.setType(value);
          }
        }
        break;
      case "len":
        data.setLen(par.getIntValue());
        break;
      default:
        readOther(data, par, fieldName);
    }
  }

  public final NativeResponse.Link.Builder readRespLink(JsonParser par) throws IOException {
    NativeResponse.Link.Builder link = NativeResponse.Link.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespLinkField(par, link, fieldName);
      }
    }
    return link;
  }

  protected void readRespLinkField(
      JsonParser par, NativeResponse.Link.Builder link, String fieldName) throws IOException {
    switch (fieldName) {
      case "url":
        link.setUrl(par.getText());
        break;
      case "clicktrackers":
        for (startArray(par); endArray(par); par.nextToken()) {
          link.addClicktrackers(par.getText());
        }
        break;
      case "fallback":
        link.setFallback(par.getText());
        break;
      default:
        readOther(link, par, fieldName);
    }
  }

  public final NativeResponse.EventTracker.Builder readRespEventTracker(JsonParser par)
      throws IOException {
    NativeResponse.EventTracker.Builder tracker = NativeResponse.EventTracker.newBuilder();
    for (startObject(par); endObject(par); par.nextToken()) {
      String fieldName = getCurrentName(par);
      if (par.nextToken() != JsonToken.VALUE_NULL) {
        readRespEventTrackerField(par, tracker, fieldName);
      }
    }
    return tracker;
  }

  protected void readRespEventTrackerField(
      JsonParser par, NativeResponse.EventTracker.Builder tracker, String fieldName)
          throws IOException {
    switch (fieldName) {
      case "event": {
          EventType value = EventType.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            tracker.setEvent(value);
          }
        }
        break;
      case "method": {
          EventTrackingMethod value = EventTrackingMethod.forNumber(par.getIntValue());
          if (checkEnum(value)) {
            tracker.setMethod(value);
          }
        }
        break;
      case "url":
        tracker.setUrl(par.getText());
        break;
      default:
        readOther(tracker, par, fieldName);
    }
  }

  protected final OpenRtbJsonReader coreReader() {
    if (coreReader == null) {
      coreReader = factory().newReader();
    }
    return coreReader;
  }
}