/*
 * 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.endObject;
import static com.google.openrtb.json.OpenRtbJsonUtils.startObject;

import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.google.openrtb.util.OpenRtbUtils;
import com.google.protobuf.GeneratedMessageV3.ExtendableBuilder;
import java.io.IOException;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Desserializes OpenRTB messages from JSON.
 */
public abstract class AbstractOpenRtbJsonReader {
  static final Logger logger = LoggerFactory.getLogger(AbstractOpenRtbJsonReader.class);
  private final OpenRtbJsonFactory factory;

  protected AbstractOpenRtbJsonReader(OpenRtbJsonFactory factory) {
    this.factory = factory;
  }

  public final OpenRtbJsonFactory factory() {
    return factory;
  }

  protected final <EB extends ExtendableBuilder<?, EB>>
  void readOther(EB msg, JsonParser par, String fieldName) throws IOException {
    if ("ext".equals(fieldName)) {
      readExtensions(msg, par);
    } else {
      par.skipChildren();
    }
  }

  /**
   * Read any extensions that may exist in a message.
   *
   * @param msg Builder of a message that may contain extensions
   * @param par The JSON parser, positioned at the "ext" field
   * @param <EB> Type of message builder being constructed
   * @throws IOException any parsing error
   */
  protected final <EB extends ExtendableBuilder<?, EB>>
  void readExtensions(EB msg, JsonParser par) throws IOException {
    @SuppressWarnings("unchecked")
    Set<OpenRtbJsonExtReader<EB>> extReaders = factory.getReaders((Class<EB>) msg.getClass());
    if (extReaders.isEmpty()) {
      par.skipChildren();
      return;
    }

    startObject(par);
    JsonToken tokLast = par.getCurrentToken();
    JsonLocation locLast = par.getCurrentLocation();

    while (true) {
      boolean extRead = false;
      for (OpenRtbJsonExtReader<EB> extReader : extReaders) {
        if (extReader.filter(par)) {
          extReader.read(msg, par);
          JsonToken tokNew = par.getCurrentToken();
          JsonLocation locNew = par.getCurrentLocation();
          boolean advanced = tokNew != tokLast || !locNew.equals(locLast);
          extRead |= advanced;

          if (!endObject(par)) {
            return;
          } else if (advanced && par.getCurrentToken() != JsonToken.FIELD_NAME) {
            tokLast = par.nextToken();
            locLast = par.getCurrentLocation();
          } else {
            tokLast = tokNew;
            locLast = locNew;
          }
        }
      }

      if (!endObject(par)) {
        // Can't rely on this exit condition inside the for loop because no readers may filter.
        return;
      }

      if (!extRead) {
        // No field was consumed by any reader, so we need to skip the field to make progress.
        if (logger.isDebugEnabled()) {
          logger.debug("Extension field not consumed by any reader, skipping: {} @{}:{}",
              par.getCurrentName(), locLast.getLineNr(), locLast.getCharOffset());
        }
        par.nextToken();
        par.skipChildren();
        tokLast = par.nextToken();
        locLast = par.getCurrentLocation();
      }
      // Else loop, try all readers again
    }
  }

  protected final boolean checkEnum(Enum<?> e) {
    if (e == null) {
      if (factory.isStrict()) {
        throw new IllegalArgumentException("Invalid enumerated value");
      } else {
        return false;
      }
    }
    return true;
  }

  protected final boolean checkContentCategory(String cat) {
    if (OpenRtbUtils.categoryFromName(cat) == null) {
      if (factory.isStrict()) {
        throw new IllegalArgumentException("Invalid ContentCategory value");
      } else {
        return false;
      }
    }
    return true;
  }

  /**
   * Special case for empty-string input. Returning null in non-@Nullable method,
   * but this is non-strict mode anyway.
   */
  protected final boolean emptyToNull(JsonParser par) throws IOException {
    JsonToken token = par.getCurrentToken();
    if (token == null) {
      token = par.nextToken();
    }
    return !factory().isStrict() && token == null;
  }
}