/*
 * Copyright 2020 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
 *
 *     https://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.android.gnd.persistence.remote.firestore.schema;

import static com.google.android.gnd.persistence.remote.DataStoreException.checkNotEmpty;
import static com.google.android.gnd.persistence.remote.DataStoreException.checkNotNull;

import com.google.android.gnd.model.feature.Feature;
import com.google.android.gnd.model.form.Form;
import com.google.android.gnd.model.observation.MultipleChoiceResponse;
import com.google.android.gnd.model.observation.Observation;
import com.google.android.gnd.model.observation.ResponseMap;
import com.google.android.gnd.model.observation.TextResponse;
import com.google.android.gnd.persistence.remote.DataStoreException;
import com.google.firebase.firestore.DocumentSnapshot;
import java.util.List;
import java.util.Map;
import java8.util.Objects;
import javax.annotation.Nullable;
import timber.log.Timber;

/** Converts between Firestore documents and {@link Observation} instances. */
class ObservationConverter {

  static Observation toObservation(Feature feature, DocumentSnapshot snapshot)
      throws DataStoreException {
    ObservationDocument doc = snapshot.toObject(ObservationDocument.class);
    String featureId = checkNotNull(doc.getFeatureId(), "featureId");
    String formId = checkNotNull(doc.getFormId(), "formId");
    if (!feature.getId().equals(featureId)) {
      Timber.e("Observation featureId doesn't match specified feature id");
    }
    Form form = checkNotEmpty(feature.getLayer().getForm(formId), "form");
    // Degrade gracefully when audit info missing in remote db.
    AuditInfoNestedObject created =
        Objects.requireNonNullElse(doc.getCreated(), AuditInfoNestedObject.FALLBACK_VALUE);
    AuditInfoNestedObject lastModified = Objects.requireNonNullElse(doc.getLastModified(), created);
    return Observation.newBuilder()
        .setId(snapshot.getId())
        .setProject(feature.getProject())
        .setFeature(feature)
        .setForm(form)
        .setResponses(toResponseMap(form, doc.getResponses()))
        .setCreated(AuditInfoConverter.toAuditInfo(created))
        .setLastModified(AuditInfoConverter.toAuditInfo(lastModified))
        .build();
  }

  private static ResponseMap toResponseMap(Form form, @Nullable Map<String, Object> docResponses) {
    ResponseMap.Builder responses = ResponseMap.builder();
    if (docResponses == null) {
      return responses.build();
    }
    for (String fieldId : docResponses.keySet()) {
      Object obj = docResponses.get(fieldId);
      if (obj instanceof String) {
        TextResponse.fromString(((String) obj).trim())
            .ifPresent(r -> responses.putResponse(fieldId, r));
        // TODO(#23): Add support for number fields:
        // } else if (obj instanceof Float) {
        //   responses.put(key, new NumericResponse((Float) obj));
      } else if (obj instanceof List) {
        MultipleChoiceResponse.fromList((List<String>) obj)
            .ifPresent(r -> responses.putResponse(fieldId, r));
      } else {
        Timber.e("Unsupported obj in db: %s", obj.getClass().getName());
      }
    }
    return responses.build();
  }
}

@@ -836,16 +836,70 @@
 NotNull;
+%0Aimport static java8.util.stream.StreamSupport.stream;
 %0A%0Aimport
@@ -945,16 +945,64 @@
 eature;%0A
+import com.google.android.gnd.model.form.Field;%0A
 import c
@@ -2677,16 +2677,34 @@
 onseMap(
+snapshot.getId(), 
 form, do
@@ -2920,16 +2920,45 @@
 onseMap(
+%0A      String observationId, 
 Form for
@@ -3196,374 +3196,716 @@
     
-Object obj = docResponses.get(fieldId);%0A      if (obj instanceof String) %7B%0A        TextResponse.fromString(((String) obj).trim())%0A            .ifPresent(r -%3E responses.putResponse(fieldId, r));%0A        // TODO(#23): Add support for number fields:%0A        // %7D else if (obj instanceof Float) %7B%0A        //   responses.put(key, new NumericResponse((Float)
+try %7B%0A        Field field =%0A            form.getField(fieldId).orElseThrow(() -%3E new DataStoreException(%22Not defined in form%22));%0A        Object obj = docResponses.get(fieldId);%0A        switch (field.getType()) %7B%0A          case PHOTO:%0A            // Intentional fall-through.%0A            // TODO(#755): Handle photo fields as PhotoResponse instead of TextResponse.%0A          case TEXT:%0A            String value = (String) DataStoreException.checkType(String.class, obj);%0A            TextResponse.fromString(value.trim()).ifPresent(r -%3E responses.putResponse(fieldId, r));%0A            break;%0A          case MULTIPLE_CHOICE:%0A            List values = (List) DataStoreException.checkType(List.class,
  obj)
-)
 ;%0A      
 %7D el
@@ -3904,42 +3904,94 @@
     
-%7D else if (obj instanceof List) %7B%0A
+      stream(values).forEach(v -%3E DataStoreException.checkType(String.class, v));%0A    
     
@@ -4045,13 +4045,20 @@
 g%3E) 
-obj)%0A
+values)%0A    
     
@@ -4127,87 +4127,383 @@
     
-%7D else %7B%0A        Timber.e(%22Unsupported obj in db: %25s%22, obj.getClass().getName()
+      break;%0A            // TODO(#23): Add support for number fields.%0A            // TODO(#748): Add support for date and time fields.%0A          default:%0A            throw new DataStoreException(%22Unknown type %22 + field.getType());%0A        %7D%0A      %7D catch (DataStoreException e) %7B%0A        Timber.d(%22Field %22 + fieldId + %22in remote db in observation %22 + observationId + %22: %22 + e
 );%0A 

Use form field defn when deserializing remote data