Insert unqualified (logical) ids for resources instead of fully qualified ids

**Describe the bug**
Upon sync, fully qualified IDs are being inserted into the database (in Resource, Index and Local change tables). This leads to crashes when deserialised resources are updated.

**Component**
Core library

**To Reproduce**
Steps to reproduce the behavior:
1. Sync resources from the HAPI FHIR server (happening automatically in the reference app.
2. Modify any resource property e.g. `birthDate`.
3. Call `FhirEngine.update(resource)`
4. Crash while selecting old resource for update.

**Root cause**

Calling `resource.id` on synced resources read from a `Bundle` yields fully qualified id, e.g. `http://hapi.fhir.org/baseR4/Patient/1915026`.
Upon serialisation the base URL is removed and only the logical part is stored in the JSON representation of the resource. E.g.:
```
{
  "resourceType": "Patient",
  "id": "1915026",
 ...
```
After deserialising this JSON, calling `resource.id` would yield `1915026`. This will not match with the stored, fully qualified id leading to crashes and the database being essentially unusable.

**Expected behavior**
Synced resources should be update-able via `FhirEngine.update(resource)`

**Screenshots**
N/A

**Smartphone (please complete the following information):**
 - Device:  Pixel4a
 - Android version: 11

**Additional context**
None.

diff --git a/core/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt b/core/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt
index 2671118ee..c7ea7ff2d 100644
--- a/core/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt
+++ b/core/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt
@@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.android.fhir.FhirServices
 import com.google.android.fhir.db.ResourceNotFoundInDbException
 import com.google.android.fhir.db.impl.entities.LocalChangeEntity
+import com.google.android.fhir.logicalId
 import com.google.android.fhir.resource.TestingUtils
 import com.google.android.fhir.sync.FhirDataSource
 import com.google.common.truth.Truth.assertThat
@@ -158,9 +159,9 @@ class DatabaseImplTest {
     database.update(patient)
     val patientString = services.parser.encodeResourceToString(patient)
     val (_, resourceType, resourceId, _, type, payload) =
-      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.id) }.second
+      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.logicalId) }.second
     assertThat(type).isEqualTo(LocalChangeEntity.Type.INSERT)
-    assertThat(resourceId).isEqualTo(patient.id)
+    assertThat(resourceId).isEqualTo(patient.logicalId)
     assertThat(resourceType).isEqualTo(patient.resourceType.name)
     assertThat(payload).isEqualTo(patientString)
   }
@@ -196,9 +197,11 @@ class DatabaseImplTest {
     database.update(patient)
     services.parser.encodeResourceToString(patient)
     val (token, _) =
-      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.id) }
+      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.logicalId) }
     database.deleteUpdates(token)
-    assertThat(database.getAllLocalChanges().none { it.second.resourceId.equals(patient.id) })
+    assertThat(
+        database.getAllLocalChanges().none { it.second.resourceId.equals(patient.logicalId) }
+      )
       .isTrue()
   }

@@ -207,7 +210,9 @@ class DatabaseImplTest {
     val patient: Patient = testingUtils.readFromFile(Patient::class.java, "/date_test_patient.json")
     database.insertRemote(patient)
     assertThat(
-        database.getAllLocalChanges().map { it.second }.none { it.resourceId.equals(patient.id) }
+        database.getAllLocalChanges().map { it.second }.none {
+          it.resourceId.equals(patient.logicalId)
+        }
       )
       .isTrue()
   }
@@ -218,7 +223,7 @@ class DatabaseImplTest {
     database.insertAllRemote(listOf(patient, TEST_PATIENT_2))
     assertThat(
         database.getAllLocalChanges().map { it.second }.none {
-          it.resourceId in listOf(patient.id, TEST_PATIENT_2_ID)
+          it.resourceId in listOf(patient.logicalId, TEST_PATIENT_2_ID)
         }
       )
       .isTrue()
@@ -233,9 +238,9 @@ class DatabaseImplTest {
     val updatePatch = testingUtils.readJsonArrayFromFile("/update_patch_1.json")
     database.update(updatedPatient)
     val (_, resourceType, resourceId, _, type, payload) =
-      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.id) }.second
+      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.logicalId) }.second
     assertThat(type).isEqualTo(LocalChangeEntity.Type.UPDATE)
-    assertThat(resourceId).isEqualTo(patient.id)
+    assertThat(resourceId).isEqualTo(patient.logicalId)
     assertThat(resourceType).isEqualTo(patient.resourceType.name)
     testingUtils.assertJsonArrayEqualsIgnoringOrder(JSONArray(payload), updatePatch)
   }
@@ -250,9 +255,9 @@ class DatabaseImplTest {
     database.update(patient)
     val updatePatch = testingUtils.readJsonArrayFromFile("/update_patch_2.json")
     val (_, resourceType, resourceId, _, type, payload) =
-      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.id) }.second
+      database.getAllLocalChanges().single { it.second.resourceId.equals(patient.logicalId) }.second
     assertThat(type).isEqualTo(LocalChangeEntity.Type.UPDATE)
-    assertThat(resourceId).isEqualTo(patient.id)
+    assertThat(resourceId).isEqualTo(patient.logicalId)
     assertThat(resourceType).isEqualTo(patient.resourceType.name)
     testingUtils.assertJsonArrayEqualsIgnoringOrder(JSONArray(payload), updatePatch)
   }
diff --git a/core/src/main/java/com/google/android/fhir/Util.kt b/core/src/main/java/com/google/android/fhir/Util.kt
index 1ee5c7bf2..495231428 100644
--- a/core/src/main/java/com/google/android/fhir/Util.kt
+++ b/core/src/main/java/com/google/android/fhir/Util.kt
@@ -21,6 +21,7 @@ import java.time.ZoneId
 import java.time.format.DateTimeFormatter
 import java.util.Date
 import java.util.Locale
+import org.hl7.fhir.r4.model.Resource

 /** Utility function to format a [Date] object using the system's default locale. */
 @SuppressLint("NewApi")
@@ -30,3 +31,10 @@ internal fun Date.toTimeZoneString(): String {
       .withZone(ZoneId.systemDefault())
   return simpleDateFormat.format(this.toInstant())
 }
+
+/**
+ * The logical (unqualified) part of the ID. For example, if the ID is
+ * "http://example.com/fhir/Patient/123/_history/456", then this value would be "123".
+ */
+internal val Resource.logicalId: String
+  get() = this.idElement.idPart
diff --git a/core/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt b/core/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt
index 6486e339c..2ed49e583 100644
--- a/core/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt
+++ b/core/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt
@@ -25,6 +25,7 @@ import com.google.android.fhir.db.impl.dao.LocalChangeToken
 import com.google.android.fhir.db.impl.dao.LocalChangeUtils
 import com.google.android.fhir.db.impl.entities.LocalChangeEntity
 import com.google.android.fhir.db.impl.entities.SyncedResourceEntity
+import com.google.android.fhir.logicalId
 import com.google.android.fhir.resource.getResourceType
 import com.google.android.fhir.search.impl.Query
 import org.hl7.fhir.r4.model.Resource
@@ -79,7 +80,7 @@ internal class DatabaseImpl(context: Context, private val iParser: IParser, data

   @Transaction
   override fun <R : Resource> update(resource: R) {
-    val oldResource = select(resource.javaClass, resource.id)
+    val oldResource = select(resource.javaClass, resource.logicalId)
     resourceDao.update(resource)
     localChangeDao.addUpdate(oldResource, resource)
   }
@@ -157,8 +158,8 @@ internal class DatabaseImpl(context: Context, private val iParser: IParser, data
     codeSystem: String,
     codeValue: String
   ): List<R> {
-    val refs = searchByReference(clazz, reference, referenceValue).map { it.id }
-    return searchByCode(clazz, code, codeSystem, codeValue).filter { refs.contains(it.id) }
+    val refs = searchByReference(clazz, reference, referenceValue).map { it.logicalId }
+    return searchByCode(clazz, code, codeSystem, codeValue).filter { refs.contains(it.logicalId) }
   }

   override fun <R : Resource> search(query: Query): List<R> =
diff --git a/core/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt b/core/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt
index fac940d6b..5c4c42956 100644
--- a/core/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt
+++ b/core/src/main/java/com/google/android/fhir/db/impl/dao/LocalChangeDao.kt
@@ -24,6 +24,7 @@ import androidx.room.Transaction
 import ca.uhn.fhir.parser.IParser
 import com.google.android.fhir.db.impl.entities.LocalChangeEntity
 import com.google.android.fhir.db.impl.entities.LocalChangeEntity.Type
+import com.google.android.fhir.logicalId
 import com.google.android.fhir.toTimeZoneString
 import java.util.Date
 import org.hl7.fhir.r4.model.Resource
@@ -48,7 +49,7 @@ internal abstract class LocalChangeDao {
   }

   fun addInsert(resource: Resource) {
-    val resourceId = resource.id
+    val resourceId = resource.logicalId
     val resourceType = resource.resourceType
     val timestamp = Date().toTimeZoneString()
     val resourceString = iParser.encodeResourceToString(resource)
@@ -66,7 +67,7 @@ internal abstract class LocalChangeDao {
   }

   fun addUpdate(oldResource: Resource, resource: Resource) {
-    val resourceId = resource.id
+    val resourceId = resource.logicalId
     val resourceType = resource.resourceType
     val timestamp = Date().toTimeZoneString()

diff --git a/core/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt b/core/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt
index 68f55f53f..34aa78b86 100644
--- a/core/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt
+++ b/core/src/main/java/com/google/android/fhir/db/impl/dao/ResourceDao.kt
@@ -34,6 +34,7 @@ import com.google.android.fhir.db.impl.entities.TokenIndexEntity
 import com.google.android.fhir.db.impl.entities.UriIndexEntity
 import com.google.android.fhir.index.ResourceIndexer
 import com.google.android.fhir.index.ResourceIndices
+import com.google.android.fhir.logicalId
 import org.hl7.fhir.r4.model.Resource
 import org.hl7.fhir.r4.model.ResourceType

@@ -45,12 +46,16 @@ internal abstract class ResourceDao {

   @Transaction
   open fun update(resource: Resource) {
-    updateResource(resource.id, resource.resourceType, iParser.encodeResourceToString(resource))
+    updateResource(
+      resource.logicalId,
+      resource.resourceType,
+      iParser.encodeResourceToString(resource)
+    )
     val entity =
       ResourceEntity(
         id = 0,
         resourceType = resource.resourceType,
-        resourceId = resource.id,
+        resourceId = resource.logicalId,
         serializedResource = iParser.encodeResourceToString(resource)
       )
     val index = ResourceIndexer.index(resource)
@@ -180,7 +185,7 @@ internal abstract class ResourceDao {
       ResourceEntity(
         id = 0,
         resourceType = resource.resourceType,
-        resourceId = resource.id,
+        resourceId = resource.logicalId,
         serializedResource = iParser.encodeResourceToString(resource)
       )
     insertResource(entity)
diff --git a/core/src/main/java/com/google/android/fhir/index/ResourceIndexer.kt b/core/src/main/java/com/google/android/fhir/index/ResourceIndexer.kt
index 964eb6168..085eb5c94 100644
--- a/core/src/main/java/com/google/android/fhir/index/ResourceIndexer.kt
+++ b/core/src/main/java/com/google/android/fhir/index/ResourceIndexer.kt
@@ -26,6 +26,7 @@ import com.google.android.fhir.index.entities.ReferenceIndex
 import com.google.android.fhir.index.entities.StringIndex
 import com.google.android.fhir.index.entities.TokenIndex
 import com.google.android.fhir.index.entities.UriIndex
+import com.google.android.fhir.logicalId
 import java.math.BigDecimal
 import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext
 import org.hl7.fhir.r4.model.Base
@@ -55,7 +56,7 @@ internal object ResourceIndexer {
   fun <R : Resource> index(resource: R) = extractIndexValues(resource)

   private fun <R : Resource> extractIndexValues(resource: R): ResourceIndices {
-    val indexBuilder = ResourceIndices.Builder(resource.resourceType, resource.id)
+    val indexBuilder = ResourceIndices.Builder(resource.resourceType, resource.logicalId)
     resource
       .javaClass
       .fields

Insert unqualified (logical) ids for resources instead of fully qualified ids
**Describe the bug**
Upon sync, fully qualified IDs are being inserted into the database (in Resource, Index and Local change tables). This leads to crashes when deserialised resources are updated.

**Component**
Core library

**To Reproduce**
Steps to reproduce the behavior:
1. Sync resources from the HAPI FHIR server (happening automatically in the reference app.
2. Modify any resource property e.g. `birthDate`.
3. Call `FhirEngine.update(resource)`
4. Crash while selecting old resource for update.

**Root cause**

Calling `resource.id` on synced resources read from a `Bundle` yields fully qualified id, e.g. `http://hapi.fhir.org/baseR4/Patient/1915026`.
Upon serialisation the base URL is removed and only the logical part is stored in the JSON representation of the resource. E.g.:
```
{
  "resourceType": "Patient",
  "id": "1915026",
 ...
```
After deserialising this JSON, calling `resource.id` would yield `1915026`. This will not match with the stored, fully qualified id leading to crashes and the database being essentially unusable.

**Expected behavior**
Synced resources should be update-able via `FhirEngine.update(resource)`

**Screenshots**
N/A

**Smartphone (please complete the following information):**
 - Device:  Pixel4a
 - Android version: 11

**Additional context**
None.

/*
 * 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
 *
 *       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.fhir.ucum

import org.hl7.fhir.exceptions.FHIRException

/** Needed for HAPI's FHIRPathEngine. See https://github.com/hapifhir/hapi-fhir/issues/2443. */
class UcumException : FHIRException {
  constructor() {}
  constructor(message: String?, cause: Throwable?) : super(message, cause) {}
  constructor(message: String?) : super(message) {}
  constructor(cause: Throwable?) : super(cause) {}
}

/*
 * 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
 *
 *       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 java.awt

class Image

/*
 * 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
 *
 *       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.android.fhir

import com.google.android.fhir.search.Search
import com.google.android.fhir.sync.PeriodicSyncConfiguration
import com.google.android.fhir.sync.Result
import com.google.android.fhir.sync.SyncConfiguration
import org.hl7.fhir.r4.model.Resource

/** The FHIR Engine interface that handles the local storage of FHIR resources. */
interface FhirEngine {
  /**
   * Saves a FHIR `resource` in the local storage. If the resource already exists, it will be
   * overwritten
   *
   * @param <R> The resource type which should be a subtype of [Resource].
   */
  fun <R : Resource> save(resource: R)

  /**
   * Saves a list of FHIR `resource` in the local storage. If any of the resources already exist,
   * they will be overwritten.
   *
   * @param <R> The resource type which should be a subtype of [Resource].
   */
  fun <R : Resource> saveAll(resources: List<R>)

  /**
   * Updates a FHIR `resource` in the local storage.
   *
   * @param <R> The resource type which should be a subtype of [Resource].
   */
  fun <R : Resource> update(resource: R)

  /**
   * Returns a FHIR resource of type `clazz` with `id` from the local storage.
   *
   * @param <R> The resource type which should be a subtype of [Resource].
   * @throws ResourceNotFoundException if the resource is not found
   */
  @Throws(ResourceNotFoundException::class) fun <R : Resource> load(clazz: Class<R>, id: String): R

  /**
   * Removes a FHIR resource of type `clazz` with `id` from the local storage.
   *
   * @param <R> The resource type which should be a subtype of [Resource].
   */
  fun <R : Resource> remove(clazz: Class<R>, id: String)

  /** Returns the entry point for [Search]. */
  fun search(): Search

  /**
   * One time sync.
   *
   * @param syncConfiguration
   * - configuration of data that needs to be synchronised
   */
  suspend fun sync(syncConfiguration: SyncConfiguration): Result

  suspend fun periodicSync(): Result

  fun updatePeriodicSyncConfiguration(syncConfig: PeriodicSyncConfiguration)
}

/*
 * 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
 *
 *       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.android.fhir

import android.content.Context
import com.google.android.fhir.sync.FhirDataSource
import com.google.android.fhir.sync.PeriodicSyncConfiguration

/** The builder for [FhirEngine] instance */
class FhirEngineBuilder constructor(dataSource: FhirDataSource, context: Context) {
  private val services = FhirServices.builder(dataSource, context)

  /** Sets the database file name for the FhirEngine to use. */
  fun databaseName(name: String) = apply { services.databaseName(name) }

  /** Instructs the FhirEngine to use an in memory database which can be useful for tests. */
  internal fun inMemory() = apply { services.inMemory() }

  /** Configures the FhirEngine periodic sync. */
  fun periodicSyncConfiguration(config: PeriodicSyncConfiguration) = apply {
    services.periodicSyncConfiguration(config)
  }

  /** Builds a new instance of the [FhirEngine]. */
  fun build() = services.build().fhirEngine
}

/*
 * 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
 *
 *       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.android.fhir

import android.content.Context
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.db.Database
import com.google.android.fhir.db.impl.DatabaseImpl
import com.google.android.fhir.impl.FhirEngineImpl
import com.google.android.fhir.search.impl.SearchImpl
import com.google.android.fhir.sync.FhirDataSource
import com.google.android.fhir.sync.PeriodicSyncConfiguration

internal data class FhirServices(
  val fhirEngine: FhirEngine,
  val parser: IParser,
  val database: Database
) {
  class Builder(private val dataSource: FhirDataSource, private val context: Context) {
    private var databaseName: String? = "fhirEngine"
    private var periodicSyncConfiguration: PeriodicSyncConfiguration? = null

    fun inMemory() = apply { databaseName = null }

    fun databaseName(name: String) = apply { databaseName = name }

    fun periodicSyncConfiguration(config: PeriodicSyncConfiguration) = apply {
      periodicSyncConfiguration = config
    }

    fun build(): FhirServices {
      val parser = FhirContext.forR4().newJsonParser()
      val db = DatabaseImpl(context = context, iParser = parser, databaseName = databaseName)
      val engine =
        FhirEngineImpl(
          database = db,
          search = SearchImpl(db),
          periodicSyncConfiguration = periodicSyncConfiguration,
          dataSource = dataSource,
          context = context
        )
      return FhirServices(fhirEngine = engine, parser = parser, database = db)
    }
  }

  companion object {
    fun builder(dataSource: FhirDataSource, context: Context) = Builder(dataSource, context)
  }
}

/*
 * 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
 *
 *       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.android.fhir

/** Thrown to indicate that the resource already exists. */
class ResourceAlreadyExistsException(val type: String, val id: String, cause: Throwable) :
  Exception("Resource with type $type and id $id already exists!", cause)

/*
 * 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
 *
 *       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.android.fhir

/** Thrown to indicate that the requested resource is not found. */
class ResourceNotFoundException(val type: String, val id: String, cause: Throwable) :
  Exception("Resource not found with type $type and id $id!", cause)

/*
 * 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
 *
 *       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.android.fhir

import android.annotation.SuppressLint
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.Locale

/** Utility function to format a [Date] object using the system's default locale. */
@SuppressLint("NewApi")
internal fun Date.toTimeZoneString(): String {
  val simpleDateFormat =
    DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.getDefault())
      .withZone(ZoneId.systemDefault())
  return simpleDateFormat.format(this.toInstant())
}

/*
 * 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
 *
 *       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 java.awt.datatransfer

interface Transferable

/*
 * 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
 *
 *       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.android.fhir.db

import com.google.android.fhir.db.impl.dao.LocalChangeToken
import com.google.android.fhir.db.impl.entities.LocalChangeEntity
import com.google.android.fhir.db.impl.entities.SyncedResourceEntity
import com.google.android.fhir.search.impl.Query
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType

/** The interface for the FHIR resource database. */
interface Database {
  /**
   * Inserts the local origin `resource` into the FHIR resource database. If the resource already
   * exists, it will be overwritten.
   *
   * @param <R> The resource type
   */
  fun <R : Resource> insert(resource: R)

  /**
   * Inserts the remote origin `resource` into the FHIR resource database. If the resource already
   * exists, it will be overwritten.
   *
   * @param <R> The resource type
   */
  fun <R : Resource> insertRemote(resource: R)

  /**
   * Inserts a list of local `resources` into the FHIR resource database. If any of the resources
   * already exists, it will be overwritten.
   *
   * @param <R> The resource type
   */
  fun <R : Resource> insertAll(resources: List<R>)

  /**
   * Inserts a list of remote `resources` into the FHIR resource database. If any of the resources
   * already exists, it will be overwritten.
   *
   * @param <R> The resource type
   */
  fun <R : Resource> insertAllRemote(resources: List<R>)

  /**
   * Updates the `resource` in the FHIR resource database. If the resource does not already exist,
   * then it will not be created.
   *
   * @param <R> The resource type
   */
  fun <R : Resource> update(resource: R)

  /**
   * Selects the FHIR resource of type `clazz` with `id`.
   *
   * @param <R> The resource type
   * @throws ResourceNotFoundInDbException if the resource is not found in the database
   */
  @Throws(ResourceNotFoundInDbException::class)
  fun <R : Resource> select(clazz: Class<R>, id: String): R

  /**
   * Return the last update data of a resource based on the resource type. If no resource of
   * [resourceType] is inserted, return `null`.
   * @param resourceType The resource type
   */
  suspend fun lastUpdate(resourceType: ResourceType): String?

  /**
   * Insert a resource that was syncronised.
   *
   * @param syncedResourceEntity The synced resource
   */
  suspend fun insertSyncedResources(
    syncedResourceEntity: SyncedResourceEntity,
    resources: List<Resource>
  )

  /**
   * Deletes the FHIR resource of type `clazz` with `id`.
   *
   * @param <R> The resource type
   */
  fun <R : Resource> delete(clazz: Class<R>, id: String)

  /**
   * Returns a [List] of [Resource] s that are of type `clazz` and have `reference` with `value`.
   *
   * For example, a search for [org.hl7.fhir.r4.model.Observation] s with `reference` 'subject' and
   * `value` 'Patient/1' will return all observations associated with the particular patient.
   */
  fun <R : Resource> searchByReference(clazz: Class<R>, reference: String, value: String): List<R>

  /**
   * Returns a [List] of [Resource] s that are of type `clazz` and have `string` with `value`.
   *
   * For example, a search for [org.hl7.fhir.r4.model.Patient] s with `string` 'given' and `value`
   * 'Tom' will return all patients with a given name Tom.
   */
  fun <R : Resource> searchByString(clazz: Class<R>, string: String, value: String): List<R>

  /**
   * Returns a [List] of [Resource] s that are of type `clazz` and have `code` with `system` and
   * `value`.
   *
   * For example, a search for [org.hl7.fhir.r4.model.Observation] s with `code` 'code', `system`
   * 'http://openmrs.org/concepts' and `value` '1427AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' will return
   * all observations with the given code.
   */
  fun <R : Resource> searchByCode(
    clazz: Class<R>,
    code: String,
    system: String,
    value: String
  ): List<R>

  /**
   * Returns a [List] of [Resource] s that are of type `clazz` and have `reference` with `value` and
   * `code` with `system` and `value`.
   *
   * For example, a search for [org.hl7.fhir.r4.model.Observation] s with `reference` 'subject' and
   * `value` 'Patient/1' as well as with `code` 'code', `system` 'http://openmrs.org/concepts' and
   * `value` '1427AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' will return all observations associated with the
   * particular patient by reference and with the given code.
   */
  fun <R : Resource> searchByReferenceAndCode(
    clazz: Class<R>,
    reference: String,
    referenceValue: String,
    code: String,
    codeSystem: String,
    codeValue: String
  ): List<R>

  fun <R : Resource> search(query: Query): List<R>

  /**
   * Retrieves all [LocalChangeEntity] s for all [Resource] s, which can be used to update the
   * remote FHIR server. Each [resource] will have at most one
   * [LocalChangeEntity](multiple changes
   * are squashed).
   */
  fun getAllLocalChanges(): List<Pair<LocalChangeToken, LocalChangeEntity>>

  /** Remove the [LocalChangeEntity] s with given ids. Call this after a successful sync. */
  fun deleteUpdates(token: LocalChangeToken)
}

/*
 * 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
 *
 *       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.android.fhir.db

/** Exception thrown to indicate that the resource already exists in the database. */
class ResourceAlreadyExistsInDbException(val type: String, val id: String, cause: Throwable) :
  Exception("Resource with type $type and id $id already exists!", cause)

/*
 * 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
 *
 *       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.android.fhir.db

/** Exception thrown to indicate that the requested resource is not found in the database. */
class ResourceNotFoundInDbException(val type: String, val id: String) :
  Exception("Resource not found with type $type and id $id!")

/*
 * 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
 *
 *       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.android.fhir.impl

import android.content.Context
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.ResourceNotFoundException
import com.google.android.fhir.db.Database
import com.google.android.fhir.db.ResourceNotFoundInDbException
import com.google.android.fhir.resource.getResourceType
import com.google.android.fhir.search.Search
import com.google.android.fhir.sync.FhirDataSource
import com.google.android.fhir.sync.FhirSynchronizer
import com.google.android.fhir.sync.PeriodicSyncConfiguration
import com.google.android.fhir.sync.Result
import com.google.android.fhir.sync.SyncConfiguration
import com.google.android.fhir.sync.SyncWorkType
import org.hl7.fhir.r4.model.Resource

/** Implementation of [FhirEngine]. */
class FhirEngineImpl
constructor(
  private val database: Database,
  private val search: Search,
  private var periodicSyncConfiguration: PeriodicSyncConfiguration?,
  private val dataSource: FhirDataSource,
  private val context: Context
) : FhirEngine {

  init {
    periodicSyncConfiguration?.let { config -> triggerInitialDownload(config) }
  }

  override fun <R : Resource> save(resource: R) {
    database.insert(resource)
  }

  override fun <R : Resource> saveAll(resources: List<R>) {
    database.insertAll(resources)
  }

  override fun <R : Resource> update(resource: R) {
    database.update(resource)
  }

  @Throws(ResourceNotFoundException::class)
  override fun <R : Resource> load(clazz: Class<R>, id: String): R {
    return try {
      database.select(clazz, id)
    } catch (e: ResourceNotFoundInDbException) {
      throw ResourceNotFoundException(getResourceType(clazz).name, id, e)
    }
  }

  override fun <R : Resource> remove(clazz: Class<R>, id: String) {
    database.delete(clazz, id)
  }

  override fun search(): Search {
    return search
  }

  override suspend fun sync(syncConfiguration: SyncConfiguration): Result {
    return FhirSynchronizer(syncConfiguration, dataSource, database).sync()
  }

  override suspend fun periodicSync(): Result {
    val syncConfig =
      periodicSyncConfiguration
        ?: throw java.lang.UnsupportedOperationException("Periodic sync configuration was not set")
    val syncResult = FhirSynchronizer(syncConfig.syncConfiguration, dataSource, database).sync()
    setupNextDownload(syncConfig)
    return syncResult
  }

  override fun updatePeriodicSyncConfiguration(syncConfig: PeriodicSyncConfiguration) {
    periodicSyncConfiguration = syncConfig
    setupNextDownload(syncConfig)
  }

  private fun setupNextDownload(syncConfig: PeriodicSyncConfiguration) {
    setupDownload(syncConfig = syncConfig, withInitialDelay = true)
  }

  private fun triggerInitialDownload(syncConfig: PeriodicSyncConfiguration) {
    setupDownload(syncConfig = syncConfig, withInitialDelay = false)
  }

  private fun setupDownload(syncConfig: PeriodicSyncConfiguration, withInitialDelay: Boolean) {
    val workerClass = syncConfig.periodicSyncWorker
    val downloadRequest =
      if (withInitialDelay) {
        OneTimeWorkRequest.Builder(workerClass)
          .setConstraints(syncConfig.syncConstraints)
          .setInitialDelay(syncConfig.repeat.interval, syncConfig.repeat.timeUnit)
          .build()
      } else {
        OneTimeWorkRequest.Builder(workerClass).setConstraints(syncConfig.syncConstraints).build()
      }

    WorkManager.getInstance(context)
      .enqueueUniqueWork(SyncWorkType.DOWNLOAD.workerName, ExistingWorkPolicy.KEEP, downloadRequest)
  }
}

/*
 * 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
 *
 *       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.android.fhir.index

import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport
import ca.uhn.fhir.model.api.annotation.SearchParamDefinition
import com.google.android.fhir.index.entities.DateIndex
import com.google.android.fhir.index.entities.NumberIndex
import com.google.android.fhir.index.entities.QuantityIndex
import com.google.android.fhir.index.entities.ReferenceIndex
import com.google.android.fhir.index.entities.StringIndex
import com.google.android.fhir.index.entities.TokenIndex
import com.google.android.fhir.index.entities.UriIndex
import java.math.BigDecimal
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.CodeableConcept
import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.DecimalType
import org.hl7.fhir.r4.model.Identifier
import org.hl7.fhir.r4.model.InstantType
import org.hl7.fhir.r4.model.IntegerType
import org.hl7.fhir.r4.model.Money
import org.hl7.fhir.r4.model.Quantity
import org.hl7.fhir.r4.model.Reference
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.UriType
import org.hl7.fhir.r4.model.codesystems.SearchParamType
import org.hl7.fhir.r4.utils.FHIRPathEngine

/**
 * Indexes a FHIR resource according to the
 * [search parameters](https://www.hl7.org/fhir/searchparameter-registry.html).
 */
internal object ResourceIndexer {
  private val context = FhirContext.forR4()
  private val fhirPathEngine =
    FHIRPathEngine(HapiWorkerContext(context, DefaultProfileValidationSupport(context)))

  fun <R : Resource> index(resource: R) = extractIndexValues(resource)

  private fun <R : Resource> extractIndexValues(resource: R): ResourceIndices {
    val indexBuilder = ResourceIndices.Builder(resource.resourceType, resource.id)
    resource
      .javaClass
      .fields
      .asSequence()
      .mapNotNull { it.getAnnotation(SearchParamDefinition::class.java) }
      .filter { it.path.isNotEmpty() }
      .map { it to fhirPathEngine.evaluate(resource, it.path) }
      .flatMap { pair -> pair.second.map { pair.first to it } }
      .forEach { pair ->
        val (searchParam, value) = pair
        when (SearchParamType.fromCode(pair.first.type)) {
          SearchParamType.NUMBER ->
            numberIndex(searchParam, value)?.also { indexBuilder.addNumberIndex(it) }
          SearchParamType.DATE ->
            dateIndex(searchParam, value)?.also { indexBuilder.addDateIndex(it) }
          SearchParamType.STRING ->
            stringIndex(searchParam, value)?.also { indexBuilder.addStringIndex(it) }
          SearchParamType.TOKEN ->
            tokenIndex(searchParam, value).forEach { indexBuilder.addTokenIndex(it) }
          SearchParamType.REFERENCE ->
            referenceIndex(searchParam, value)?.also { indexBuilder.addReferenceIndex(it) }
          SearchParamType.QUANTITY ->
            quantityIndex(searchParam, value)?.also { indexBuilder.addQuantityIndex(it) }
          SearchParamType.URI -> uriIndex(searchParam, value)?.also { indexBuilder.addUriIndex(it) }
        // TODO: Handle composite type https://github.com/google/android-fhir/issues/292.
        // TODO: Handle special type https://github.com/google/android-fhir/issues/293.
        }
      }

    // Add 'lastUpdated' index to all resources.
    if (resource.meta.hasLastUpdated()) {
      val lastUpdatedElement = resource.meta.lastUpdatedElement
      indexBuilder.addDateIndex(
        DateIndex(
          name = "_lastUpdated",
          path = arrayOf(resource.fhirType(), "meta", "lastUpdated").joinToString(separator = "."),
          tsHigh = lastUpdatedElement.value.time,
          tsLow = lastUpdatedElement.value.time,
          temporalPrecision = lastUpdatedElement.precision
        )
      )
    }

    return indexBuilder.build()
  }

  private fun numberIndex(searchParam: SearchParamDefinition, value: Base): NumberIndex? =
    when (value.fhirType()) {
      "integer" ->
        NumberIndex(searchParam.name, searchParam.path, BigDecimal((value as IntegerType).value))
      "decimal" -> NumberIndex(searchParam.name, searchParam.path, (value as DecimalType).value)
      else -> null
    }

  private fun dateIndex(searchParam: SearchParamDefinition, value: Base): DateIndex? =
    when (value.fhirType()) {
      "date" -> {
        val date = value as DateType
        DateIndex(
          searchParam.name,
          searchParam.path,
          date.value.time,
          date.value.time,
          date.precision
        )
      }
      "instant" -> {
        val instant = value as InstantType
        DateIndex(
          searchParam.name,
          searchParam.path,
          instant.value.time,
          instant.value.time,
          instant.precision
        )
      }
      else -> null
    }

  private fun stringIndex(searchParam: SearchParamDefinition, value: Base): StringIndex? =
    if (!value.isEmpty) {
      StringIndex(searchParam.name, searchParam.path, value.toString())
    } else {
      null
    }

  private fun tokenIndex(searchParam: SearchParamDefinition, value: Base): List<TokenIndex> =
    when (value.fhirType()) {
      "boolean" ->
        listOf(
          TokenIndex(searchParam.name, searchParam.path, system = null, value.primitiveValue())
        )
      "Identifier" -> {
        val identifier = value as Identifier
        if (identifier.value != null) {
          listOf(
            TokenIndex(searchParam.name, searchParam.path, identifier.system, identifier.value)
          )
        } else {
          listOf()
        }
      }
      "CodeableConcept" -> {
        val codeableConcept = value as CodeableConcept
        codeableConcept.coding.filter { it.code != null && it.code!!.isNotEmpty() }.map {
          TokenIndex(searchParam.name, searchParam.path, it.system ?: "", it.code)
        }
      }
      else -> listOf()
    }

  private fun referenceIndex(searchParam: SearchParamDefinition, value: Base): ReferenceIndex? {
    val reference = (value as Reference).reference
    return reference?.let { ReferenceIndex(searchParam.name, searchParam.path, it) }
  }

  private fun quantityIndex(searchParam: SearchParamDefinition, value: Base): QuantityIndex? =
    when (value.fhirType()) {
      "Money" -> {
        val money = value as Money
        QuantityIndex(
          searchParam.name,
          searchParam.path,
          FHIR_CURRENCY_CODE_SYSTEM,
          money.currency,
          money.value
        )
      }
      "Quantity" -> {
        val quantity = value as Quantity
        QuantityIndex(
          searchParam.name,
          searchParam.path,
          quantity.system ?: "",
          quantity.unit ?: "",
          quantity.value
        )
      }
      else -> null
    }

  private fun uriIndex(searchParam: SearchParamDefinition, value: Base?): UriIndex? {
    val uri = (value as UriType).value
    return if (uri.isNotEmpty()) {
      UriIndex(searchParam.name, searchParam.path, uri)
    } else {
      null
    }
  }

  /**
   * The FHIR currency code system. See: https://bit.ly/30YB3ML. See:
   * https://www.hl7.org/fhir/valueset-currencies.html.
   */
  private const val FHIR_CURRENCY_CODE_SYSTEM = "urn:iso:std:iso:4217"
}

/*
 * 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
 *
 *       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.android.fhir.index

import com.google.android.fhir.index.entities.DateIndex
import com.google.android.fhir.index.entities.NumberIndex
import com.google.android.fhir.index.entities.QuantityIndex
import com.google.android.fhir.index.entities.ReferenceIndex
import com.google.android.fhir.index.entities.StringIndex
import com.google.android.fhir.index.entities.TokenIndex
import com.google.android.fhir.index.entities.UriIndex
import org.hl7.fhir.r4.model.ResourceType

/**
 * Indices extracted from the resource of [resourceType] and [resourceId]. Used to create index
 * records in the database to support search.
 *
 * See https://www.hl7.org/fhir/search.html.
 */
internal data class ResourceIndices(
  val resourceType: ResourceType,
  val resourceId: String,
  val numberIndices: List<NumberIndex>,
  val dateIndices: List<DateIndex>,
  val stringIndices: List<StringIndex>,
  val uriIndices: List<UriIndex>,
  val tokenIndices: List<TokenIndex>,
  val quantityIndices: List<QuantityIndex>,
  val referenceIndices: List<ReferenceIndex>
) {
  class Builder(private val resourceType: ResourceType, private val resourceId: String) {
    private val stringIndices = mutableListOf<StringIndex>()
    private val referenceIndices = mutableListOf<ReferenceIndex>()
    private val tokenIndices = mutableListOf<TokenIndex>()
    private val quantityIndices = mutableListOf<QuantityIndex>()
    private val uriIndices = mutableListOf<UriIndex>()
    private val dateIndices = mutableListOf<DateIndex>()
    private val numberIndices = mutableListOf<NumberIndex>()

    fun addNumberIndex(numberIndex: NumberIndex) = numberIndices.add(numberIndex)
    fun addDateIndex(dateIndex: DateIndex) = dateIndices.add(dateIndex)
    fun addStringIndex(stringIndex: StringIndex) = stringIndices.add(stringIndex)
    fun addUriIndex(uriIndex: UriIndex) = uriIndices.add(uriIndex)
    fun addTokenIndex(tokenIndex: TokenIndex) = tokenIndices.add(tokenIndex)
    fun addQuantityIndex(quantityIndex: QuantityIndex) = quantityIndices.add(quantityIndex)
    fun addReferenceIndex(referenceIndex: ReferenceIndex) = referenceIndices.add(referenceIndex)

    fun build() =
      ResourceIndices(
        resourceType = resourceType,
        resourceId = resourceId,
        numberIndices = numberIndices.toList(),
        dateIndices = dateIndices.toList(),
        stringIndices = stringIndices.toList(),
        uriIndices = uriIndices.toList(),
        tokenIndices = tokenIndices.toList(),
        quantityIndices = quantityIndices.toList(),
        referenceIndices = referenceIndices.toList()
      )
  }
}

/*
 * 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
 *
 *       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.android.fhir.resource

import java.lang.reflect.InvocationTargetException
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType

/** The HAPI Fhir package prefix for R4 resources. */
internal val R4_RESOURCE_PACKAGE_PREFIX = "org.hl7.fhir.r4.model."

/** Returns the FHIR resource type. */
internal fun <R : Resource> getResourceType(clazz: Class<R>): ResourceType {
  try {
    return clazz.getConstructor().newInstance().resourceType
  } catch (e: NoSuchMethodException) {
    throw IllegalArgumentException("Cannot resolve resource type for " + clazz.name, e)
  } catch (e: IllegalAccessException) {
    throw IllegalArgumentException("Cannot resolve resource type for " + clazz.name, e)
  } catch (e: InstantiationException) {
    throw IllegalArgumentException("Cannot resolve resource type for " + clazz.name, e)
  } catch (e: InvocationTargetException) {
    throw IllegalArgumentException("Cannot resolve resource type for " + clazz.name, e)
  }
}

/** Returns the {@link Class} object for the resource type. */
internal fun <R : Resource> getResourceClass(resourceType: String): Class<R> {
  // Remove any curly brackets in the resource type string. This is to work around an issue with
  // JSON deserialization in the CQL engine on Android. The resource type string incorrectly
  // includes namespace prefix in curly brackets, e.g. "{http://hl7.org/fhir}Patient" instead of
  // "Patient".
  // TODO: remove this once a fix has been found for the CQL engine on Android.
  val className = resourceType.replace(Regex("\\{[^}]*\\}"), "")
  return Class.forName(R4_RESOURCE_PACKAGE_PREFIX + className) as Class<R>
}

/*
 * 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
 *
 *       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.android.fhir.search

import com.google.android.fhir.search.filter.FilterCriterion
import com.google.android.fhir.search.sort.SortCriterion
import org.hl7.fhir.r4.model.Resource

/**
 * The interface that acts as an entry point for search.
 *
 * Example usage:
 * ```
 * search
 *   .of(...resource type...)        // Mandatory, must be the first function call, specify type
 *   .filter(...search criteria...)  // Optional, specify search criteria
 *   .sorted(...sorting criteria...) // Optional, specify sorting criteria
 *   .skip(...number of results to skip...)     // Optional, specify number of resources to skip
 *   .limit(...number of results to return...)  // Optional, specify number of resources to return
 *   .run();                         // Mandatory, must be the last function call
 * ```
 */
interface Search {
  /** Returns a [SearchSpecifications] object with the given [clazz]. */
  fun <R : Resource> of(clazz: Class<R>): SearchSpecifications

  /** The interface to specify the search criteria and to execute the search. */
  interface SearchSpecifications {
    /** Returns a [SearchSpecifications] object with the [filterCriterion]. */
    fun filter(filterCriterion: FilterCriterion): SearchSpecifications

    /** Returns a [SearchSpecifications] object with the [sortCriterion]. */
    fun sort(sortCriterion: SortCriterion): SearchSpecifications

    /** Returns a [SearchSpecifications] object that only includes the first [limit] results. */
    fun limit(limit: Int): SearchSpecifications

    /** Returns a [SearchSpecifications] object that skips the first [skip] results. */
    fun skip(skip: Int): SearchSpecifications

    /** Runs a search with the [SearchSpecifications]. */
    fun <R : Resource> run(): List<R>
  }
}

/*
 * 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
 *
 *       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.android.fhir.sync

import org.hl7.fhir.r4.model.Bundle

/**
 * Interface for an abstraction of retrieving static data from a network source. The data can be
 * retrieved in pages and each data retrieval is an expensive operation.
 */
interface FhirDataSource {

  /**
   * Implement this method to load remote data based on a url [path]. A service base url is of the
   * form: `http{s}://server/{path}`
   */
  suspend fun loadData(path: String): Bundle
}

/*
 * 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
 *
 *       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.android.fhir.sync

import com.google.android.fhir.db.Database
import org.hl7.fhir.r4.model.ResourceType

sealed class Result {
  object Success : Result()
  data class Error(val exceptions: List<ResourceSyncException>) : Result()
}

data class ResourceSyncException(val resourceType: ResourceType, val exception: Exception)

/** Class that helps synchronize the data source and save it in the local database */
class FhirSynchronizer(
  private val syncConfiguration: SyncConfiguration,
  private val dataSource: FhirDataSource,
  private val database: Database
) {
  suspend fun sync(): Result {
    val exceptions = mutableListOf<ResourceSyncException>()
    syncConfiguration.syncData.forEach { syncData ->
      val resourceSynchroniser =
        ResourceSynchronizer(syncData, dataSource, database, syncConfiguration.retry)
      try {
        resourceSynchroniser.sync()
      } catch (exception: Exception) {
        exceptions.add(ResourceSyncException(syncData.resourceType, exception))
      }
    }
    if (exceptions.isEmpty()) {
      return Result.Success
    } else {
      return Result.Error(exceptions)
    }
  }
}

/*
 * 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
 *
 *       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.android.fhir.sync

import androidx.work.Constraints
import java.util.concurrent.TimeUnit

/** Configuration for period synchronisation */
class PeriodicSyncConfiguration(
  val syncConfiguration: SyncConfiguration,
  /**
   * Constraints that specify the requirements needed before the synchronisation is triggered. E.g.
   * network type (Wifi, 3G etc), the device should be charging etc.
   */
  val syncConstraints: Constraints = Constraints.Builder().build(),

  /** Worker that will execute the periodic sync */
  val periodicSyncWorker: Class<out PeriodicSyncWorker>,

  /** The interval at which the sync should be triggered in */
  val repeat: RepeatInterval
)

data class RepeatInterval(
  /** The interval at which the sync should be triggered in */
  val interval: Long,

  /** The time unit for the repeat interval */
  val timeUnit: TimeUnit
)

/*
 * 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
 *
 *       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.android.fhir.sync

import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.sync.Result.Success

/** A WorkManager Worker that handles periodic sync. */
abstract class PeriodicSyncWorker(appContext: Context, workerParams: WorkerParameters) :
  CoroutineWorker(appContext, workerParams) {

  abstract fun getFhirEngine(): FhirEngine

  override suspend fun doWork(): Result {
    // TODO handle retry
    val result = getFhirEngine().periodicSync()
    if (result is Success) {
      return Result.success()
    }
    return Result.failure()
  }
}

/*
 * 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
 *
 *       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.android.fhir.sync

import com.google.android.fhir.db.Database
import com.google.android.fhir.db.impl.entities.SyncedResourceEntity
import com.google.android.fhir.sync.SyncData.Companion.LAST_UPDATED_ASC_VALUE
import com.google.android.fhir.sync.SyncData.Companion.LAST_UPDATED_KEY
import com.google.android.fhir.sync.SyncData.Companion.SORT_KEY
import com.google.android.fhir.toTimeZoneString
import java.io.IOException
import org.hl7.fhir.r4.model.Bundle

/** Class that synchronises only one resource. */
class ResourceSynchronizer(
  private val syncData: SyncData,
  private val dataSource: FhirDataSource,
  private val database: Database,
  retry: Boolean
) {
  private var retrySync = retry

  suspend fun sync() {
    var nextUrl: String? = getInitialUrl()
    try {
      while (nextUrl != null) {
        val bundle = dataSource.loadData(nextUrl)
        nextUrl = bundle.link.firstOrNull { component -> component.relation == "next" }?.url
        if (bundle.type == Bundle.BundleType.SEARCHSET) {
          saveSyncedResource(bundle)
        }
      }
    } catch (exception: IOException) {
      if (retrySync) {
        retrySync = false
        sync()
      } else {
        // propagate the exception upstream
        throw exception
      }
    }
  }

  private suspend fun getInitialUrl(): String? {
    val updatedSyncData = syncData.addSortParam().addLastUpdateDate()
    return "${updatedSyncData.resourceType.name}?${updatedSyncData.concatParams()}"
  }

  private fun SyncData.addSortParam(): SyncData {
    if (params.containsKey(SORT_KEY)) {
      return this
    }
    val newParams = params.toMutableMap()
    newParams[SORT_KEY] = LAST_UPDATED_ASC_VALUE
    return SyncData(resourceType, newParams)
  }

  private suspend fun SyncData.addLastUpdateDate(): SyncData {
    val lastUpdate = database.lastUpdate(resourceType)
    if (lastUpdate == null) {
      return this
    }
    val newParams = params.toMutableMap()
    newParams[LAST_UPDATED_KEY] = "gt$lastUpdate"
    return SyncData(resourceType, newParams)
  }

  private suspend fun saveSyncedResource(bundle: Bundle) {
    val resources = bundle.entry.map { it.resource }
    if (resources.isNotEmpty()) {
      val mostRecentResource = resources[resources.lastIndex]
      database.insertSyncedResources(
        SyncedResourceEntity(
          syncData.resourceType,
          mostRecentResource.meta.lastUpdated.toTimeZoneString()
        ),
        resources
      )
    }
  }
}

/*
 * 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
 *
 *       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.android.fhir.sync

/** Configuration for synchronization. */
data class SyncConfiguration(
  /** Data that needs to be synchronised */
  val syncData: List<SyncData> = emptyList(),
  /**
   * true if the SDK needs to retry a failed sync attempt, false otherwise If this is set to true,
   * then the result of the sync will be reported after the retry.
   */
  val retry: Boolean = false
)

/*
 * 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
 *
 *       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.android.fhir.sync

import java.net.URLEncoder
import org.hl7.fhir.r4.model.ResourceType

fun SyncData.concatParams(): String {
  return this.params.entries.joinToString("&") { (key, value) ->
    "$key=${URLEncoder.encode(value, "UTF-8")}"
  }
}

/**
 * Class that holds what type of resources we need to synchronise and what are the parameters of
 * that type. e.g. we only want to synchronise patients that live in United States
 * `SyncData(ResourceType.Patient, mapOf("address-country" to "United States")`
 */
data class SyncData(val resourceType: ResourceType, val params: Map<String, String> = emptyMap()) {
  companion object {
    const val SORT_KEY = "_sort"
    const val LAST_UPDATED_KEY = "_lastUpdated"
    const val ADDRESS_COUNTRY_KEY = "address-country"
    const val LAST_UPDATED_ASC_VALUE = "_lastUpdated"
  }
}

/*
 * 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
 *
 *       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.android.fhir.sync

/** Defines different types of synchronisation workers: download and upload */
internal enum class SyncWorkType(val workerName: String) {
  DOWNLOAD("download"),
  UPLOAD("upload")
}

/*
 * 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
 *
 *       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.android.fhir.resource

import ca.uhn.fhir.parser.IParser
import com.google.common.truth.Truth
import org.hl7.fhir.r4.model.Resource
import org.json.JSONArray
import org.json.JSONObject

/** Utilities for testing. */
class TestingUtils constructor(private val iParser: IParser) {

  /** Asserts that the `expected` and the `actual` FHIR resources are equal. */
  fun assertResourceEquals(expected: Resource?, actual: Resource?) {
    Truth.assertThat(iParser.encodeResourceToString(actual))
      .isEqualTo(iParser.encodeResourceToString(expected))
  }

  fun assertJsonArrayEqualsIgnoringOrder(actual: JSONArray, expected: JSONArray) {
    Truth.assertThat(actual.length()).isEqualTo(expected.length())
    val actuals = mutableListOf<String>()
    val expecteds = mutableListOf<String>()
    for (i in 0 until actual.length()) {
      actuals.add(actual.get(i).toString())
      expecteds.add(expected.get(i).toString())
    }
    actuals.sorted()
    expecteds.sorted()
    Truth.assertThat(actuals).containsExactlyElementsIn(expecteds)
  }

  /** Reads a [Resource] from given file in the `sampledata` dir */
  fun <R : Resource> readFromFile(clazz: Class<R>, filename: String): R {
    val resourceJson = readJsonFromFile(filename)
    return iParser.parseResource(clazz, resourceJson.toString()) as R
  }

  /** Reads a [JSONObject] from given file in the `sampledata` dir */
  fun readJsonFromFile(filename: String): JSONObject {
    val inputStream = javaClass.getResourceAsStream(filename)
    val content = inputStream!!.bufferedReader(Charsets.UTF_8).readText()
    return JSONObject(content)
  }

  /** Reads a [JSONArray] from given file in the `sampledata` dir */
  fun readJsonArrayFromFile(filename: String): JSONArray {
    val inputStream = javaClass.getResourceAsStream(filename)
    val content = inputStream!!.bufferedReader(Charsets.UTF_8).readText()
    return JSONArray(content)
  }
}

/*
 * 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
 *
 *       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.android.fhir.cqlreference

import android.os.AsyncTask
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import ca.uhn.fhir.context.FhirContext
import com.google.android.fhir.cqlreference.FhirApplication.Companion.fhirEngine
import com.google.android.fhir.cqlreference.cql.CqlEngineUtils
import com.google.android.material.snackbar.Snackbar
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.net.URL
import org.hl7.fhir.r4.model.Resource

class CqlLoadActivity : AppCompatActivity() {
  lateinit var fhirEngine: FhirEngine
  lateinit var cqlLibraryUrlInput: EditText
  lateinit var fhirResourceUrlInput: EditText
  lateinit var libraryInput: EditText
  lateinit var contextInput: EditText
  lateinit var expressionInput: EditText
  lateinit var evaluationResultTextView: TextView
  lateinit var cqlEngineUtils: CqlEngineUtils

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_cql_load)

    fhirEngine = fhirEngine(this)
    cqlEngineUtils = CqlEngineUtils(this)
    cqlLibraryUrlInput = findViewById(R.id.cql_text_input)
    fhirResourceUrlInput = findViewById(R.id.fhir_resource_url_input)
    libraryInput = findViewById(R.id.library_input)
    contextInput = findViewById(R.id.context_input)
    expressionInput = findViewById(R.id.expression_input)
    evaluationResultTextView = findViewById(R.id.evaluate_result)

    val loadCqlLibButton = findViewById<Button>(R.id.load_cql_lib_button)
    loadCqlLibButton.setOnClickListener {
      DownloadFhirResource().execute(cqlLibraryUrlInput.text.toString())
    }

    val downloadFhirResourceButton = findViewById<Button>(R.id.download_fhir_resource_button)
    downloadFhirResourceButton.setOnClickListener {
      DownloadFhirResource().execute(fhirResourceUrlInput.text.toString())
    }

    val evaluateButton = findViewById<Button>(R.id.evaluate_button)
    evaluateButton.setOnClickListener {
      Snackbar.make(this, it, "Not implemented", Snackbar.LENGTH_LONG).show()
      //      EvaluateAncLibrary()
      //        .execute(
      //          libraryInput.text.toString(),
      //          contextInput.text.toString(),
      //          expressionInput.text.toString()
    }
  }

  private inner class DownloadFhirResource : AsyncTask<String?, String?, Void?>() {
    override fun doInBackground(vararg strings: String?): Void? {
      var result: String? = ""
      var stream: InputStream? = null
      var resource: Resource? = null
      try {
        stream = URL(strings[0]).content as InputStream
        val reader = BufferedReader(InputStreamReader(stream))
        var line: String? = ""
        while (line != null) {
          result += line
          line = reader.readLine()
        }
        val fhirContext = FhirContext.forR4()
        resource =
          fhirContext.newJsonParser().parseResource(result) as org.hl7.fhir.r4.model.Resource
        fhirEngine.save(resource)
        Snackbar.make(
            cqlLibraryUrlInput,
            "Loaded " + resource.resourceType.name + " with ID " + resource.id,
            Snackbar.LENGTH_SHORT
          )
          .show()
      } catch (e: IOException) {
        e.printStackTrace()
        Snackbar.make(cqlLibraryUrlInput, "Something went wrong...", Snackbar.LENGTH_SHORT).show()
      }
      return null
    }
  }

  //  private inner class EvaluateAncLibrary : AsyncTask<String?, String?, EvaluationResult?>() {
  //    override fun doInBackground(vararg strings: String?): EvaluationResult? {
  //      return cqlEngineUtils.evaluateCql(strings[0]!!, strings[1]!!, strings[2]!!)
  //    }
  //
  //    override fun onPostExecute(result: EvaluationResult?) {
  //      val stringBuilder = StringBuilder()
  //      if (result?.libraryResults?.values != null) {
  //        for (libraryResult in result.libraryResults.values) {
  //          for ((key, value) in libraryResult.expressionResults) {
  //            stringBuilder.append("$key -> ")
  //            if (value == null) {
  //              stringBuilder.append("null")
  //            } else if (MutableList::class.java.isAssignableFrom(value.javaClass)) {
  //              for (listItem in value as List<*>) {
  //                stringBuilder.append(
  //                  FhirContext.forR4().newJsonParser().encodeResourceToString(listItem as
  // Resource?)
  //                )
  //              }
  //            } else if (Resource::class.java.isAssignableFrom(value.javaClass)) {
  //              stringBuilder.append(
  //                FhirContext.forR4().newJsonParser().encodeResourceToString(value as Resource)
  //              )
  //            } else {
  //              stringBuilder.append(value.toString())
  //            }
  //          }
  //        }
  //      }
  //      evaluationResultTextView.text = stringBuilder.toString()
  //    }
  //  }
}

/*
 * 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
 *
 *       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.android.fhir.cqlreference

import android.app.Application
import android.content.Context

class FhirApplication : Application() {

  val fhirEngine: FhirEngine by lazy { constructFhirEngine() }

  fun constructFhirEngine(): FhirEngine {
    return FhirEngineBuilder(this).build()
  }

  companion object {
    fun fhirEngine(context: Context) = (context.applicationContext as FhirApplication).fhirEngine
  }
}

/*
 * 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
 *
 *       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.android.fhir.cqlreference

import org.hl7.fhir.r4.model.Resource

interface FhirEngine {
  fun <R : Resource> save(resource: R)
}

/*
 * 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
 *
 *       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.android.fhir.cqlreference

import android.content.Context

class FhirEngineBuilder constructor(context: Context) {
  private val services = FhirServices.builder(context)
  fun build() = services.build().fhirEngine
}

/*
 * 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
 *
 *       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.android.fhir.cqlreference

import android.content.Context
import org.hl7.fhir.r4.model.Resource

class FhirEngineImp constructor(context: Context) : FhirEngine {
  override fun <R : Resource> save(resource: R) {
    // TODO: implement all methods as well using db
  }
}

/*
 * 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
 *
 *       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.android.fhir.cqlreference

import android.content.Context

internal data class FhirServices(val fhirEngine: FhirEngine) {
  class Builder constructor(private val context: Context) {
    fun build(): FhirServices = FhirServices(FhirEngineImp(context))
  }

  companion object {
    fun builder(context: Context) = Builder(context)
  }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture

import org.hl7.fhir.r4.model.Questionnaire

val Questionnaire.QuestionnaireItemAnswerOptionComponent.displayString: String
  get() {
    if (hasValueCoding()) {
      val display = valueCoding.display
      return if (display.isNullOrEmpty()) {
        valueCoding.code
      } else {
        display
      }
    } else {
      throw IllegalArgumentException("Answer option does not having coding.")
    }
  }

/*
 * 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
 *
 *       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.android.fhir.datacapture

import org.hl7.fhir.r4.model.CodeableConcept
import org.hl7.fhir.r4.model.Questionnaire

internal const val ITEM_CONTROL_DROP_DOWN = "drop-down"
internal const val ITEM_CONTROL_RADIO_BUTTON = "radio-button"

internal const val EXTENSION_ITEM_CONTROL_URL =
  "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl"
internal const val EXTENSION_ITEM_CONTROL_SYSTEM = "http://hl7.org/fhir/questionnaire-item-control"

// Item control code as string or null
internal val Questionnaire.QuestionnaireItemComponent.itemControl: String?
  get() {
    val codeableConcept =
      this.extension.firstOrNull { it.url == EXTENSION_ITEM_CONTROL_URL }?.value as CodeableConcept?
    val code =
      codeableConcept?.coding?.firstOrNull { it.system == EXTENSION_ITEM_CONTROL_SYSTEM }?.code
    return listOf(ITEM_CONTROL_DROP_DOWN, ITEM_CONTROL_RADIO_BUTTON).firstOrNull { it == code }
  }

/*
 * 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
 *
 *       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.android.fhir.datacapture

import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse

/**
 * Returns the value of the [Questionnaire.Item.EnableWhen.AnswerX] for the [type].
 *
 * @throws IllegalArgumentException if [type] is not supported (for example, questions of type
 * [QuestionnaireItemTypeCode.Value.URL] do not have an explicit EnableWhen answer).
 */
fun Questionnaire.QuestionnaireItemEnableWhenComponent.getValueForType(
  type: Questionnaire.QuestionnaireItemType
): Base =
  when (type) {
    Questionnaire.QuestionnaireItemType.DATE -> answerDateType
    Questionnaire.QuestionnaireItemType.BOOLEAN -> answerBooleanType
    Questionnaire.QuestionnaireItemType.DECIMAL -> answerDecimalType
    Questionnaire.QuestionnaireItemType.INTEGER -> answerIntegerType
    Questionnaire.QuestionnaireItemType.DATETIME -> answerDateTimeType
    Questionnaire.QuestionnaireItemType.TIME -> answerTimeType
    Questionnaire.QuestionnaireItemType.CHOICE -> answerCoding
    Questionnaire.QuestionnaireItemType.STRING, Questionnaire.QuestionnaireItemType.TEXT ->
      answerStringType
    else -> throw IllegalArgumentException("Unsupported value type $type")
  }

/**
 * Returns the value of the [QuestionnaireResponse.Item.Answer] for the [type].
 *
 * @throws IllegalArgumentException if [type] is not supported (for example, questions of type
 * [QuestionnaireItemTypeCode.Value.GROUP] do not collect any answer).
 */
fun QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent.getValueForType(
  type: Questionnaire.QuestionnaireItemType
): Base =
  when (type) {
    Questionnaire.QuestionnaireItemType.DATE -> valueDateType
    Questionnaire.QuestionnaireItemType.BOOLEAN -> valueBooleanType
    Questionnaire.QuestionnaireItemType.DECIMAL -> valueDecimalType
    Questionnaire.QuestionnaireItemType.INTEGER -> valueIntegerType
    Questionnaire.QuestionnaireItemType.DATETIME -> valueDateTimeType
    Questionnaire.QuestionnaireItemType.TIME -> valueTimeType
    Questionnaire.QuestionnaireItemType.CHOICE -> valueCoding
    Questionnaire.QuestionnaireItemType.STRING, Questionnaire.QuestionnaireItemType.TEXT ->
      this.valueStringType
    Questionnaire.QuestionnaireItemType.URL -> this.valueUriType
    else -> throw IllegalArgumentException("Unsupported value type $value")
  }

/*
 * 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
 *
 *       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.android.fhir.datacapture

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.res.use
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.flow.collect

class QuestionnaireFragment : Fragment() {
  private val viewModel: QuestionnaireViewModel by viewModels()

  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View {
    inflater.context.obtainStyledAttributes(R.styleable.QuestionnaireTheme).use {
      val themeId =
        it.getResourceId(
          // Use the custom questionnaire theme if it is specified
          R.styleable.QuestionnaireTheme_questionnaire_theme,
          // Otherwise, use the default questionnaire theme
          R.style.Theme_Questionnaire
        )
      return inflater
        .cloneInContext(ContextThemeWrapper(inflater.context, themeId))
        .inflate(R.layout.questionnaire_fragment, container, false)
    }
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
    val adapter = QuestionnaireItemAdapter()
    recyclerView.adapter = adapter
    recyclerView.layoutManager = LinearLayoutManager(view.context)

    // Listen to updates from the view model.
    viewLifecycleOwner.lifecycleScope.launchWhenCreated {
      viewModel.questionnaireItemViewItemListFlow.collect { adapter.submitList(it) }
    }
  }

  // Returns the current questionnaire response
  fun getQuestionnaireResponse() = viewModel.getQuestionnaireResponse()

  companion object {
    const val BUNDLE_KEY_QUESTIONNAIRE = "questionnaire"
    const val BUNDLE_KEY_QUESTIONNAIRE_RESPONSE = "questionnaire-response"
  }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture

import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.google.android.fhir.datacapture.views.QuestionnaireItemCheckBoxViewHolderFactory
import com.google.android.fhir.datacapture.views.QuestionnaireItemDatePickerViewHolderFactory
import com.google.android.fhir.datacapture.views.QuestionnaireItemDateTimePickerViewHolderFactory
import com.google.android.fhir.datacapture.views.QuestionnaireItemDisplayViewHolderFactory
import com.google.android.fhir.datacapture.views.QuestionnaireItemDropDownViewHolderFactory
import com.google.android.fhir.datacapture.views.QuestionnaireItemEditTextDecimalViewHolderFactory
import com.google.android.fhir.datacapture.views.QuestionnaireItemEditTextIntegerViewHolderFactory
import com.google.android.fhir.datacapture.views.QuestionnaireItemEditTextMultiLineViewHolderFactory
import com.google.android.fhir.datacapture.views.QuestionnaireItemEditTextQuantityViewHolderFactory
import com.google.android.fhir.datacapture.views.QuestionnaireItemEditTextSingleLineViewHolderFactory
import com.google.android.fhir.datacapture.views.QuestionnaireItemGroupViewHolderFactory
import com.google.android.fhir.datacapture.views.QuestionnaireItemRadioGroupViewHolderFactory
import com.google.android.fhir.datacapture.views.QuestionnaireItemViewHolder
import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType

internal class QuestionnaireItemAdapter :
  ListAdapter<QuestionnaireItemViewItem, QuestionnaireItemViewHolder>(DiffCallback) {
  /**
   * @param viewType the integer value of the [QuestionnaireItemViewHolderType] used to render the
   * [QuestionnaireItemViewItem].
   */
  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionnaireItemViewHolder {
    val viewHolderFactory =
      when (QuestionnaireItemViewHolderType.fromInt(viewType)) {
        QuestionnaireItemViewHolderType.GROUP -> QuestionnaireItemGroupViewHolderFactory
        QuestionnaireItemViewHolderType.CHECK_BOX -> QuestionnaireItemCheckBoxViewHolderFactory
        QuestionnaireItemViewHolderType.DATE_PICKER -> QuestionnaireItemDatePickerViewHolderFactory
        QuestionnaireItemViewHolderType.DATE_TIME_PICKER ->
          QuestionnaireItemDateTimePickerViewHolderFactory
        QuestionnaireItemViewHolderType.EDIT_TEXT_SINGLE_LINE ->
          QuestionnaireItemEditTextSingleLineViewHolderFactory
        QuestionnaireItemViewHolderType.EDIT_TEXT_MULTI_LINE ->
          QuestionnaireItemEditTextMultiLineViewHolderFactory
        QuestionnaireItemViewHolderType.EDIT_TEXT_INTEGER ->
          QuestionnaireItemEditTextIntegerViewHolderFactory
        QuestionnaireItemViewHolderType.EDIT_TEXT_DECIMAL ->
          QuestionnaireItemEditTextDecimalViewHolderFactory
        QuestionnaireItemViewHolderType.RADIO_GROUP -> QuestionnaireItemRadioGroupViewHolderFactory
        QuestionnaireItemViewHolderType.DROP_DOWN -> QuestionnaireItemDropDownViewHolderFactory
        QuestionnaireItemViewHolderType.DISPLAY -> QuestionnaireItemDisplayViewHolderFactory
        QuestionnaireItemViewHolderType.QUANTITY ->
          QuestionnaireItemEditTextQuantityViewHolderFactory
      }
    return viewHolderFactory.create(parent)
  }

  override fun onBindViewHolder(holder: QuestionnaireItemViewHolder, position: Int) {
    holder.bind(getItem(position))
  }

  /**
   * Returns the integer value of the [QuestionnaireItemViewHolderType] that will be used to render
   * the [QuestionnaireItemViewItem]. This is determined by a combination of the data type of the
   * question and any additional Questionnaire Item UI Control Codes
   * (http://hl7.org/fhir/R4/valueset-questionnaire-item-control.html) used in the itemControl
   * extension (http://hl7.org/fhir/R4/extension-questionnaire-itemcontrol.html).
   */
  override fun getItemViewType(position: Int): Int {
    val questionnaireItem = getItem(position).questionnaireItem
    return when (val type = questionnaireItem.type) {
      QuestionnaireItemType.GROUP -> QuestionnaireItemViewHolderType.GROUP
      QuestionnaireItemType.BOOLEAN -> QuestionnaireItemViewHolderType.CHECK_BOX
      QuestionnaireItemType.DATE -> QuestionnaireItemViewHolderType.DATE_PICKER
      QuestionnaireItemType.DATETIME -> QuestionnaireItemViewHolderType.DATE_TIME_PICKER
      QuestionnaireItemType.STRING -> QuestionnaireItemViewHolderType.EDIT_TEXT_SINGLE_LINE
      QuestionnaireItemType.TEXT -> QuestionnaireItemViewHolderType.EDIT_TEXT_MULTI_LINE
      QuestionnaireItemType.INTEGER -> QuestionnaireItemViewHolderType.EDIT_TEXT_INTEGER
      QuestionnaireItemType.DECIMAL -> QuestionnaireItemViewHolderType.EDIT_TEXT_DECIMAL
      QuestionnaireItemType.CHOICE -> getChoiceViewHolderType(questionnaireItem)
      QuestionnaireItemType.DISPLAY -> QuestionnaireItemViewHolderType.DISPLAY
      QuestionnaireItemType.QUANTITY -> QuestionnaireItemViewHolderType.QUANTITY
      else -> throw NotImplementedError("Question type $type not supported.")
    }.value
  }

  private fun getChoiceViewHolderType(
    questionnaireItem: Questionnaire.QuestionnaireItemComponent
  ): QuestionnaireItemViewHolderType {
    if (questionnaireItem.itemControl == ITEM_CONTROL_DROP_DOWN) {
      return QuestionnaireItemViewHolderType.DROP_DOWN
    } else if (questionnaireItem.itemControl == ITEM_CONTROL_RADIO_BUTTON) {
      return QuestionnaireItemViewHolderType.RADIO_GROUP
    } else if (questionnaireItem.answerOption.size >= MINIMUM_NUMBER_OF_ANSWER_OPTIONS_FOR_DROP_DOWN
    ) {
      return QuestionnaireItemViewHolderType.DROP_DOWN
    } else {
      return QuestionnaireItemViewHolderType.RADIO_GROUP
    }
  }

  internal companion object {
    // Choice questions are rendered as radio group if number of options less than this constant
    const val MINIMUM_NUMBER_OF_ANSWER_OPTIONS_FOR_DROP_DOWN = 4
  }
}

internal object DiffCallback : DiffUtil.ItemCallback<QuestionnaireItemViewItem>() {
  override fun areItemsTheSame(
    oldItem: QuestionnaireItemViewItem,
    newItem: QuestionnaireItemViewItem
  ) = oldItem.questionnaireItem.linkId == newItem.questionnaireItem.linkId

  override fun areContentsTheSame(
    oldItem: QuestionnaireItemViewItem,
    newItem: QuestionnaireItemViewItem
  ) =
    oldItem.questionnaireItem.equalsDeep(newItem.questionnaireItem) &&
      oldItem.questionnaireResponseItem.equalsDeep(newItem.questionnaireResponseItem)
}

/*
 * 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
 *
 *       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.android.fhir.datacapture

/**
 * Questionnaire item view holder types supported by default by the data capture library.
 *
 * This is used in [QuestionnaireItemAdapter] to determine how each [Questionnaire.Item] is
 * rendered.
 *
 * This list should provide sufficient coverage for values in
 * https://www.hl7.org/fhir/valueset-item-type.html and
 * http://hl7.org/fhir/R4/valueset-questionnaire-item-control.html.
 */
internal enum class QuestionnaireItemViewHolderType(val value: Int) {
  GROUP(0),
  CHECK_BOX(1),
  DATE_PICKER(2),
  DATE_TIME_PICKER(3),
  EDIT_TEXT_SINGLE_LINE(4),
  EDIT_TEXT_MULTI_LINE(5),
  EDIT_TEXT_INTEGER(6),
  EDIT_TEXT_DECIMAL(7),
  RADIO_GROUP(8),
  DROP_DOWN(9),
  DISPLAY(10),
  QUANTITY(11);

  companion object {
    private val VALUES = values()
    fun fromInt(value: Int) = VALUES[value]
  }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import ca.uhn.fhir.context.FhirContext
import com.google.android.fhir.datacapture.enablement.EnablementEvaluator
import com.google.android.fhir.datacapture.enablement.QuestionnaireItemWithResponse
import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse

internal class QuestionnaireViewModel(state: SavedStateHandle) : ViewModel() {
  /** The current questionnaire as questions are being answered. */
  private val questionnaire: Questionnaire

  init {
    val questionnaireJson: String = state[QuestionnaireFragment.BUNDLE_KEY_QUESTIONNAIRE]!!
    questionnaire =
      FhirContext.forR4().newJsonParser().parseResource(questionnaireJson) as Questionnaire
  }

  /** The current questionnaire response as questions are being answered. */
  private var questionnaireResponse: QuestionnaireResponse

  init {
    val questionnaireJsonResponseString: String? =
      state[QuestionnaireFragment.BUNDLE_KEY_QUESTIONNAIRE_RESPONSE]
    if (questionnaireJsonResponseString != null) {
      questionnaireResponse =
        FhirContext.forR4().newJsonParser().parseResource(questionnaireJsonResponseString) as
          QuestionnaireResponse
      validateQuestionnaireResponseItems(questionnaire.item, questionnaireResponse.item)
    } else {
      questionnaireResponse =
        QuestionnaireResponse().apply {
          questionnaire = this@QuestionnaireViewModel.questionnaire.id
        }
      // Retain the hierarchy and order of items within the questionnaire as specified in the
      // standard. See https://www.hl7.org/fhir/questionnaireresponse.html#notes.
      questionnaire.item.forEach {
        questionnaireResponse.addItem(it.createQuestionnaireResponseItem())
      }
    }
  }

  /** Map from link IDs to questionnaire response items. */
  private val linkIdToQuestionnaireResponseItemMap =
    createLinkIdToQuestionnaireResponseItemMap(questionnaireResponse.item)

  /** Map from link IDs to questionnaire items. */
  private val linkIdToQuestionnaireItemMap = createLinkIdToQuestionnaireItemMap(questionnaire.item)

  /** Tracks modifications in order to update the UI. */
  private val modificationCount = MutableStateFlow(0)

  /** Callback function to update the UI. */
  private val questionnaireResponseItemChangedCallback = { modificationCount.value += 1 }

  internal val questionnaireItemViewItemList
    get() = getQuestionnaireItemViewItemList(questionnaire.item, questionnaireResponse.item)

  /** [QuestionnaireItemViewItem] s to be displayed in the UI. */
  internal val questionnaireItemViewItemListFlow: Flow<List<QuestionnaireItemViewItem>> =
    modificationCount.map { questionnaireItemViewItemList }

  /** The current [QuestionnaireResponse] captured by the UI. */
  fun getQuestionnaireResponse(): QuestionnaireResponse = questionnaireResponse

  private fun createLinkIdToQuestionnaireResponseItemMap(
    questionnaireResponseItemList: List<QuestionnaireResponse.QuestionnaireResponseItemComponent>
  ): Map<String, QuestionnaireResponse.QuestionnaireResponseItemComponent> {
    val linkIdToQuestionnaireResponseItemMap =
      questionnaireResponseItemList.map { it.linkId to it }.toMap().toMutableMap()
    for (item in questionnaireResponseItemList) {
      linkIdToQuestionnaireResponseItemMap.putAll(
        createLinkIdToQuestionnaireResponseItemMap(item.item)
      )
    }
    return linkIdToQuestionnaireResponseItemMap
  }

  private fun createLinkIdToQuestionnaireItemMap(
    questionnaireItemList: List<Questionnaire.QuestionnaireItemComponent>
  ): Map<String, Questionnaire.QuestionnaireItemComponent> {
    val linkIdToQuestionnaireItemMap =
      questionnaireItemList.map { it.linkId to it }.toMap().toMutableMap()
    for (item in questionnaireItemList) {
      linkIdToQuestionnaireItemMap.putAll(createLinkIdToQuestionnaireItemMap(item.item))
    }
    return linkIdToQuestionnaireItemMap
  }

  /**
   * Traverse (DFS) through the list of questionnaire items , the list of questionnaire response
   * items and the list of items in the questionnaire response answer list and populate
   * [questionnaireItemViewItemList] with matching pairs of questionnaire item and questionnaire
   * response item.
   *
   * The traverse is carried out in the two lists in tandem. The two lists should be structurally
   * identical.
   */
  private fun getQuestionnaireItemViewItemList(
    questionnaireItemList: List<Questionnaire.QuestionnaireItemComponent>,
    questionnaireResponseItemList: List<QuestionnaireResponse.QuestionnaireResponseItemComponent>
  ): List<QuestionnaireItemViewItem> {
    val questionnaireItemViewItemList = mutableListOf<QuestionnaireItemViewItem>()
    val questionnaireItemListIterator = questionnaireItemList.iterator()
    val questionnaireResponseItemListIterator = questionnaireResponseItemList.iterator()
    while (questionnaireItemListIterator.hasNext() &&
      questionnaireResponseItemListIterator.hasNext()) {
      val questionnaireItem = questionnaireItemListIterator.next()
      val questionnaireResponseItem = questionnaireResponseItemListIterator.next()

      val enabled =
        EnablementEvaluator.evaluate(questionnaireItem) { linkId ->
          QuestionnaireItemWithResponse(
            (linkIdToQuestionnaireItemMap[linkId]
              ?: return@evaluate QuestionnaireItemWithResponse(null, null)),
            (linkIdToQuestionnaireResponseItemMap[linkId]
              ?: return@evaluate QuestionnaireItemWithResponse(null, null))
          )
        }
      if (enabled) {
        questionnaireItemViewItemList.add(
          QuestionnaireItemViewItem(
            questionnaireItem,
            questionnaireResponseItem,
            questionnaireResponseItemChangedCallback
          )
        )
        questionnaireItemViewItemList.addAll(
          getQuestionnaireItemViewItemList(questionnaireItem.item, questionnaireResponseItem.item)
        )
        if (!questionnaireItem.type.equals(Questionnaire.QuestionnaireItemType.GROUP)) {
          questionnaireResponseItem.answer?.forEach {
            if (it.item.size > 0) {
              questionnaireItemViewItemList.addAll(
                getQuestionnaireItemViewItemList(questionnaireItem.item, it.item)
              )
            }
          }
        }
      }
    }
    return questionnaireItemViewItemList
  }
}

/**
 * Creates a [QuestionnaireResponse.QuestionnaireResponseItemComponent] from the provided
 * [Questionnaire.QuestionnaireItemComponent].
 *
 * The hierarchy and order of child items will be retained as specified in the standard. See
 * https://www.hl7.org/fhir/questionnaireresponse.html#notes for more details.
 */
private fun Questionnaire.QuestionnaireItemComponent.createQuestionnaireResponseItem():
  QuestionnaireResponse.QuestionnaireResponseItemComponent {
  return QuestionnaireResponse.QuestionnaireResponseItemComponent().apply {
    linkId = this@createQuestionnaireResponseItem.linkId
    this@createQuestionnaireResponseItem.item.forEach {
      this.addItem(it.createQuestionnaireResponseItem())
    }
  }
}

/**
 * Traverse (DFS) through the list of questionnaire items and the list of questionnaire response
 * items and check if the linkid of the matching pairs of questionnaire item and questionnaire
 * response item are equal. The traverse is carried out in the two lists in tandem. The two lists
 * should be structurally identical.
 */
private fun validateQuestionnaireResponseItems(
  questionnaireItemList: List<Questionnaire.QuestionnaireItemComponent>,
  questionnaireResponseItemList: List<QuestionnaireResponse.QuestionnaireResponseItemComponent>
) {
  val questionnaireItemListIterator = questionnaireItemList.iterator()
  val questionnaireResponseItemListIterator = questionnaireResponseItemList.iterator()
  while (questionnaireItemListIterator.hasNext() &&
    questionnaireResponseItemListIterator.hasNext()) {
    // TODO: Validate type and item nesting within answers for repeated answers
    // https://github.com/google/android-fhir/issues/286
    val questionnaireItem = questionnaireItemListIterator.next()
    val questionnaireResponseItem = questionnaireResponseItemListIterator.next()
    if (!questionnaireItem.linkId.equals(questionnaireResponseItem.linkId))
      throw IllegalArgumentException(
        "Mismatching linkIds for questionnaire item ${questionnaireItem.linkId} and " +
          "questionnaire response item ${questionnaireResponseItem.linkId}"
      )
    if (questionnaireItem.type.equals(Questionnaire.QuestionnaireItemType.GROUP)) {
      validateQuestionnaireResponseItems(questionnaireItem.item, questionnaireResponseItem.item)
    } else {
      validateQuestionnaireResponseItems(
        questionnaireItem.item,
        questionnaireResponseItem.answer.first().item
      )
    }
  }
  if (questionnaireItemListIterator.hasNext() xor questionnaireResponseItemListIterator.hasNext()) {
    if (questionnaireItemListIterator.hasNext()) {
      throw IllegalArgumentException(
        "No matching questionnaire response item for questionnaire item ${questionnaireItemListIterator.next().linkId}"
      )
    } else {
      throw IllegalArgumentException(
        "No matching questionnaire item for questionnaire response item ${questionnaireResponseItemListIterator.next().linkId}"
      )
    }
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.sync.SyncConfiguration
import com.google.android.fhir.sync.SyncData
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.ResourceType

class CqlActivityViewModel(private val fhirEngine: FhirEngine) : ViewModel() {

  init {
    requestPatients()
  }

  private fun requestPatients() {
    viewModelScope.launch {
      val syncData =
        listOf(
          SyncData(
            // For the purpose of demo, sync patients that live in Nairobi.
            resourceType = ResourceType.Patient,
            // add "_revinclude" to "Observation:subject" to return Observations for
            // the patients.
            params = mapOf("address-city" to "NAIROBI")
          )
        )

      val syncConfig = SyncConfiguration(syncData = syncData)
      val result = fhirEngine.sync(syncConfig)
      Log.d("CqlActivityViewModel", "sync result: $result")
    }
  }
}

class CqlLoadActivityViewModelFactory(private val fhirEngine: FhirEngine) :
  ViewModelProvider.Factory {
  override fun <T : ViewModel?> create(modelClass: Class<T>): T {
    if (modelClass.isAssignableFrom(CqlActivityViewModel::class.java)) {
      return CqlActivityViewModel(fhirEngine) as T
    }
    throw IllegalArgumentException("Unknown ViewModel class")
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference

import android.app.Application
import android.content.Context
import androidx.work.Constraints
import ca.uhn.fhir.context.FhirContext
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.FhirEngineBuilder
import com.google.android.fhir.reference.api.HapiFhirService.Companion.create
import com.google.android.fhir.reference.data.FhirPeriodicSyncWorker
import com.google.android.fhir.reference.data.HapiFhirResourceDataSource
import com.google.android.fhir.sync.FhirDataSource
import com.google.android.fhir.sync.PeriodicSyncConfiguration
import com.google.android.fhir.sync.RepeatInterval
import com.google.android.fhir.sync.SyncConfiguration
import com.google.android.fhir.sync.SyncData
import java.util.concurrent.TimeUnit
import org.hl7.fhir.r4.model.ResourceType

class FhirApplication : Application() {

  // only initiate the FhirEngine when used for the first time, not when the app is created
  private val fhirEngine: FhirEngine by lazy { constructFhirEngine() }

  private fun constructFhirEngine(): FhirEngine {
    val parser = FhirContext.forR4().newJsonParser()
    val service = create(parser)
    val params = mutableMapOf("address-city" to "NAIROBI")
    val syncData: MutableList<SyncData> = ArrayList()
    syncData.add(SyncData(ResourceType.Patient, params))
    val configuration = SyncConfiguration(syncData, false)
    val periodicSyncConfiguration =
      PeriodicSyncConfiguration(
        syncConfiguration = configuration,
        syncConstraints = Constraints.Builder().build(),
        periodicSyncWorker = FhirPeriodicSyncWorker::class.java,
        repeat = RepeatInterval(interval = 1, timeUnit = TimeUnit.HOURS)
      )
    val dataSource: FhirDataSource = HapiFhirResourceDataSource(service)
    return FhirEngineBuilder(dataSource, this)
      .periodicSyncConfiguration(periodicSyncConfiguration)
      .build()
  }

  companion object {
    fun fhirEngine(context: Context) = (context.applicationContext as FhirApplication).fhirEngine
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter

/** UI Controller helper class to display list of observations. */
class ObservationItemRecyclerViewAdapter :
  ListAdapter<PatientListViewModel.ObservationItem, ObservationItemViewHolder>(
    ObservationItemDiffCallback()
  ) {

  class ObservationItemDiffCallback :
    DiffUtil.ItemCallback<PatientListViewModel.ObservationItem>() {
    override fun areItemsTheSame(
      oldItem: PatientListViewModel.ObservationItem,
      newItem: PatientListViewModel.ObservationItem
    ): Boolean = oldItem.id == newItem.id

    override fun areContentsTheSame(
      oldItem: PatientListViewModel.ObservationItem,
      newItem: PatientListViewModel.ObservationItem
    ): Boolean = oldItem.id == newItem.id
  }

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ObservationItemViewHolder {
    val view =
      LayoutInflater.from(parent.context).inflate(R.layout.observation_list_item, parent, false)
    return ObservationItemViewHolder(view)
  }

  override fun onBindViewHolder(holder: ObservationItemViewHolder, position: Int) {
    val item = currentList[position]
    holder.bindTo(item)
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference

import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class ObservationItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
  private val observationTextView: TextView = itemView.findViewById(R.id.observation_detail)

  fun bindTo(observationItem: PatientListViewModel.ObservationItem) {
    this.observationTextView.text =
      itemView.resources.getString(
        R.string.observation_brief_text,
        observationItem.code,
        observationItem.value,
        observationItem.effective
      )
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference

import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity

/** An activity representing a single Patient detail screen. */
class PatientDetailActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_patient_detail)
    setSupportActionBar(findViewById(R.id.detail_toolbar))

    // Show the Up button in the action bar.
    supportActionBar?.setDisplayHomeAsUpEnabled(true)

    if (savedInstanceState == null) {
      // Create the detail fragment and add it to the activity
      // using a fragment transaction.
      val fragment =
        PatientDetailFragment().apply {
          arguments =
            Bundle().apply {
              putString(
                PatientDetailFragment.ARG_ITEM_ID,
                intent.getStringExtra(PatientDetailFragment.ARG_ITEM_ID)
              )
            }
        }

      supportFragmentManager
        .beginTransaction()
        .add(R.id.patient_detail_container, fragment)
        .commit()
    }
  }

  override fun onOptionsItemSelected(item: MenuItem) =
    when (item.itemId) {
      android.R.id.home -> {
        navigateUpTo(Intent(this, PatientListActivity::class.java))
        true
      }
      else -> super.onOptionsItemSelected(item)
    }
}

/*
 * 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
 *
 *       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.android.fhir.reference

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView

/**
 * A fragment representing a single Patient detail screen. This fragment is contained in a
 * [PatientDetailActivity].
 */
class PatientDetailFragment : Fragment() {

  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View? {
    val rootView = inflater.inflate(R.layout.patient_detail, container, false)

    val recyclerView: RecyclerView = rootView.findViewById(R.id.observation_list)
    val adapter = ObservationItemRecyclerViewAdapter()
    recyclerView.adapter = adapter

    var patient: PatientListViewModel.PatientItem? = null
    setupPatientData(rootView, patient)

    return rootView
  }

  private fun setupPatientData(view: View, patient: PatientListViewModel.PatientItem?) {
    if (patient != null) {
      view.findViewById<TextView>(R.id.patient_detail).text =
        HtmlCompat.fromHtml(patient.html, HtmlCompat.FROM_HTML_MODE_LEGACY)
      view.findViewById<TextView>(R.id.name).text = patient.name
      view.findViewById<TextView>(R.id.dob).text = patient.dob
      view.findViewById<TextView>(R.id.gender).text = patient.phone
    }
  }

  companion object {
    /** The fragment argument representing the patient item ID that this fragment represents. */
    const val ARG_ITEM_ID = "patient_item_id"
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter

/** UI Controller helper class to monitor Patient viewmodel and display list of patients. */
class PatientItemRecyclerViewAdapter(
  private val onItemClicked: (PatientListViewModel.PatientItem) -> Unit
) :
  ListAdapter<PatientListViewModel.PatientItem, PatientItemViewHolder>(PatientItemDiffCallback()) {

  class PatientItemDiffCallback : DiffUtil.ItemCallback<PatientListViewModel.PatientItem>() {
    override fun areItemsTheSame(
      oldItem: PatientListViewModel.PatientItem,
      newItem: PatientListViewModel.PatientItem
    ): Boolean = oldItem.id == newItem.id

    override fun areContentsTheSame(
      oldItem: PatientListViewModel.PatientItem,
      newItem: PatientListViewModel.PatientItem
    ): Boolean = oldItem.id == newItem.id
  }

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PatientItemViewHolder {
    val view =
      LayoutInflater.from(parent.context).inflate(R.layout.patient_list_item, parent, false)
    return PatientItemViewHolder(view)
  }

  override fun onBindViewHolder(holder: PatientItemViewHolder, position: Int) {
    val item = currentList[position]
    holder.bindTo(item, onItemClicked)
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference

import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class PatientItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
  private val idView: TextView = itemView.findViewById(R.id.id_patient_number)
  private val nameView: TextView = itemView.findViewById(R.id.name)
  private val genderView: TextView = itemView.findViewById(R.id.gender)
  private val dobView: TextView = itemView.findViewById(R.id.dob)

  fun bindTo(
    patientItem: PatientListViewModel.PatientItem,
    onItemClicked: (PatientListViewModel.PatientItem) -> Unit
  ) {
    this.idView.text = patientItem.id
    this.nameView.text = patientItem.name
    this.genderView.text = patientItem.gender
    this.dobView.text = patientItem.dob

    this.itemView.setOnClickListener { onItemClicked(patientItem) }
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference

import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.reference.FhirApplication.Companion.fhirEngine
import com.google.android.material.snackbar.Snackbar

/** An activity representing a list of Patients. */
class PatientListActivity : AppCompatActivity() {
  private lateinit var fhirEngine: FhirEngine
  private lateinit var patientListViewModel: PatientListViewModel

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d("PatientListActivity", "onCreate() called")
    setContentView(R.layout.activity_patient_list)

    val toolbar = findViewById<Toolbar>(R.id.toolbar)
    setSupportActionBar(toolbar)
    toolbar.title = title

    fhirEngine = fhirEngine(this)

    patientListViewModel =
      ViewModelProvider(this, PatientListViewModelFactory(this.application, fhirEngine))
        .get(PatientListViewModel::class.java)
    val recyclerView: RecyclerView = findViewById(R.id.patient_list)

    val adapter = PatientItemRecyclerViewAdapter(this::onPatientItemClicked)
    recyclerView.adapter = adapter

    patientListViewModel
      .getSearchedPatients()
      .observe(
        this,
        {
          Log.d("PatientListActivity", "Submitting ${it.count()} patient records")
          adapter.submitList(it)
        }
      )
  }

  // Click handler to help display the details about the patients from the list.
  private fun onPatientItemClicked(patientItem: PatientListViewModel.PatientItem) {
    val intent =
      Intent(this.applicationContext, PatientDetailActivity::class.java).apply {
        putExtra(PatientDetailFragment.ARG_ITEM_ID, patientItem.id)
      }
    this.startActivity(intent)
  }

  // To suppress the warning. Seems to be an issue with androidx library.
  // "MenuBuilder.setOptionalIconsVisible can only be called from within the same library group
  // prefix (referenced groupId=androidx.appcompat with prefix androidx from groupId=fhir-engine"
  @SuppressLint("RestrictedApi")
  override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    val inflater: MenuInflater = menuInflater
    inflater.inflate(R.menu.list_options_menu, menu)
    // To ensure that icons show up in the overflow options menu. Icons go missing without this.
    if (menu is MenuBuilder) {
      menu.setOptionalIconsVisible(true)
    }
    return true
  }

  override fun onOptionsItemSelected(item: MenuItem): Boolean {
    val view: View = findViewById(R.id.app_bar)
    // Handle item selection
    return if (item.itemId == R.id.sync_resources) {
      syncResources(view)
      true
    } else {
      super.onOptionsItemSelected(item)
    }
  }

  private fun syncResources(view: View) {
    Snackbar.make(view, "Getting Patients List", Snackbar.LENGTH_LONG)
      .setAction("Action", null)
      .show()
    patientListViewModel.searchPatients()
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference

import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import ca.uhn.fhir.rest.param.ParamPrefixEnum
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.reference.data.SamplePatients
import com.google.android.fhir.search.filter.string
import org.hl7.fhir.r4.model.Patient

/**
 * The ViewModel helper class for PatientItemRecyclerViewAdapter, that is responsible for preparing
 * data for UI.
 */
class PatientListViewModel(application: Application, private val fhirEngine: FhirEngine) :
  AndroidViewModel(application) {

  private val samplePatients = SamplePatients()

  private var patientResults: List<Patient> = getSearchResults()
  private var searchedPatients = samplePatients.getPatientItems(patientResults)
  private val _liveSearchedPatients: MutableLiveData<List<PatientItem>> = MutableLiveData()
  val liveSearchedPatients: LiveData<List<PatientItem>> = _liveSearchedPatients

  fun getSearchedPatients(): LiveData<List<PatientItem>> {
    searchedPatients = samplePatients.getPatientItems(patientResults)
    _liveSearchedPatients.value = searchedPatients
    Log.d(
      "PatientListViewModel",
      "getSearchedPatients(): " +
        "patientResults[${patientResults.count()}], searchedPatients[${searchedPatients
                .count()}]"
    )
    return liveSearchedPatients
  }

  private fun getSearchResults(): List<Patient> {
    val searchResults: List<Patient> =
      fhirEngine
        .search()
        .of(Patient::class.java)
        .filter(string(Patient.ADDRESS_CITY, ParamPrefixEnum.EQUAL, "NAIROBI"))
        .run()
    Log.d(
      "PatientListViewModel",
      "${searchResults.count()} search results: ${searchResults.joinToString(" ")}"
    )
    return searchResults
  }

  fun searchPatients() {
    patientResults = getSearchResults()
    searchedPatients = samplePatients.getPatientItems(patientResults)
    _liveSearchedPatients.value = searchedPatients
  }

  /** The Patient's details for display purposes. */
  data class PatientItem(
    val id: String,
    val name: String,
    val gender: String,
    val dob: String,
    val html: String,
    val phone: String
  ) {
    override fun toString(): String = name
  }

  /** The Observation's details for display purposes. */
  data class ObservationItem(
    val id: String,
    val code: String,
    val effective: String,
    val value: String
  ) {
    override fun toString(): String = code
  }
}

class PatientListViewModelFactory(
  private val application: Application,
  private val fhirEngine: FhirEngine
) : ViewModelProvider.Factory {
  override fun <T : ViewModel?> create(modelClass: Class<T>): T {
    if (modelClass.isAssignableFrom(PatientListViewModel::class.java)) {
      return PatientListViewModel(application, fhirEngine) as T
    }
    throw IllegalArgumentException("Unknown ViewModel class")
  }
}

/*
 * 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
 *
 *       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.android.fhir.db.impl

import android.content.Context
import androidx.room.Room
import androidx.room.Transaction
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.db.ResourceNotFoundInDbException
import com.google.android.fhir.db.impl.dao.LocalChangeToken
import com.google.android.fhir.db.impl.dao.LocalChangeUtils
import com.google.android.fhir.db.impl.entities.LocalChangeEntity
import com.google.android.fhir.db.impl.entities.SyncedResourceEntity
import com.google.android.fhir.resource.getResourceType
import com.google.android.fhir.search.impl.Query
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType

/**
 * The implementation for the persistence layer using Room. See docs for
 * [com.google.android.fhir.db.Database] for the API docs.
 */
internal class DatabaseImpl(context: Context, private val iParser: IParser, databaseName: String?) :
  com.google.android.fhir.db.Database {
  constructor(
    context: Context,
    iParser: IParser
  ) : this(context = context, iParser = iParser, databaseName = DEFAULT_DATABASE_NAME)

  val builder =
    if (databaseName == null) {
      Room.inMemoryDatabaseBuilder(context, ResourceDatabase::class.java)
    } else {
      Room.databaseBuilder(context, ResourceDatabase::class.java, databaseName)
    }
  val db =
    builder
      // TODO https://github.com/jingtang10/fhir-engine/issues/32
      //  don't allow main thread queries
      .allowMainThreadQueries()
      .build()
  val resourceDao by lazy { db.resourceDao().also { it.iParser = iParser } }
  val syncedResourceDao = db.syncedResourceDao()
  val localChangeDao = db.localChangeDao().also { it.iParser = iParser }

  @Transaction
  override fun <R : Resource> insert(resource: R) {
    resourceDao.insert(resource)
    localChangeDao.addInsert(resource)
  }

  override fun <R : Resource> insertRemote(resource: R) {
    resourceDao.insert(resource)
  }

  @Transaction
  override fun <R : Resource> insertAll(resources: List<R>) {
    resourceDao.insertAll(resources)
    localChangeDao.addInsertAll(resources)
  }

  override fun <R : Resource> insertAllRemote(resources: List<R>) {
    resourceDao.insertAll(resources)
  }

  @Transaction
  override fun <R : Resource> update(resource: R) {
    val oldResource = select(resource.javaClass, resource.id)
    resourceDao.update(resource)
    localChangeDao.addUpdate(oldResource, resource)
  }

  override fun <R : Resource> select(clazz: Class<R>, id: String): R {
    val type = getResourceType(clazz)
    return resourceDao.getResource(resourceId = id, resourceType = type)?.let {
      iParser.parseResource(clazz, it)
    }
      ?: throw ResourceNotFoundInDbException(type.name, id)
  }

  override suspend fun lastUpdate(resourceType: ResourceType): String? {
    return syncedResourceDao.getLastUpdate(resourceType)
  }

  @Transaction
  override suspend fun insertSyncedResources(
    syncedResourceEntity: SyncedResourceEntity,
    resources: List<Resource>
  ) {
    syncedResourceDao.insert(syncedResourceEntity)
    insertAll(resources)
  }

  @Transaction
  override fun <R : Resource> delete(clazz: Class<R>, id: String) {
    val type = getResourceType(clazz)
    val rowsDeleted = resourceDao.deleteResource(resourceId = id, resourceType = type)
    if (rowsDeleted > 0) localChangeDao.addDelete(resourceId = id, resourceType = type)
  }

  override fun <R : Resource> searchByReference(
    clazz: Class<R>,
    reference: String,
    value: String
  ): List<R> {
    return resourceDao.getResourceByReferenceIndex(getResourceType(clazz).name, reference, value)
      .map { iParser.parseResource(it) as R }
  }

  override fun <R : Resource> searchByString(
    clazz: Class<R>,
    string: String,
    value: String
  ): List<R> {
    return resourceDao.getResourceByStringIndex(
        resourceType = getResourceType(clazz).name,
        indexPath = string,
        indexValue = value
      )
      .map { iParser.parseResource(it) as R }
  }

  override fun <R : Resource> searchByCode(
    clazz: Class<R>,
    code: String,
    system: String,
    value: String
  ): List<R> {
    return resourceDao.getResourceByCodeIndex(
        resourceType = getResourceType(clazz).name,
        indexPath = code,
        indexSystem = system,
        indexValue = value
      )
      .map { iParser.parseResource(it) as R }
  }

  override fun <R : Resource> searchByReferenceAndCode(
    clazz: Class<R>,
    reference: String,
    referenceValue: String,
    code: String,
    codeSystem: String,
    codeValue: String
  ): List<R> {
    val refs = searchByReference(clazz, reference, referenceValue).map { it.id }
    return searchByCode(clazz, code, codeSystem, codeValue).filter { refs.contains(it.id) }
  }

  override fun <R : Resource> search(query: Query): List<R> =
    resourceDao.getResources(query.getSupportSQLiteQuery()).map { iParser.parseResource(it) as R }

  /**
   * @returns a list of pairs. Each pair is a token + squashed local change. Each token is a list of
   * [LocalChangeEntity.id] s of rows of the [LocalChangeEntity].
   */
  // TODO: create a data class for squashed local change and merge token in to it.
  override fun getAllLocalChanges(): List<Pair<LocalChangeToken, LocalChangeEntity>> =
    localChangeDao.getAllLocalChanges().groupBy { it.resourceId to it.resourceType }.values.map {
      LocalChangeToken(it.map { it.id }) to LocalChangeUtils.squash(it)
    }

  override fun deleteUpdates(token: LocalChangeToken) {
    localChangeDao.discardLocalChanges(token)
  }

  companion object {
    private const val DEFAULT_DATABASE_NAME = "ResourceDatabase"
  }
}

/*
 * 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
 *
 *       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.android.fhir.db.impl

import androidx.room.TypeConverter
import ca.uhn.fhir.model.api.TemporalPrecisionEnum
import com.google.android.fhir.db.impl.entities.LocalChangeEntity
import java.math.BigDecimal
import java.util.Calendar
import org.hl7.fhir.r4.model.ResourceType

/**
 * Type converters for Room to persist ResourceType as a string. see:
 * https://developer.android.com/training/data-storage/room/referencing-data
 */
internal object DbTypeConverters {
  private val resourceTypeLookup = ResourceType.values().associateBy { it.name }

  /**
   * Converts a [ResourceType] into a String to be persisted in the database. This allows us to save
   * [ResourceType] into the database while keeping it as the real type in entities.
   */
  @JvmStatic @TypeConverter fun typeToString(resourceType: ResourceType) = resourceType.name

  /** Converts a String into a [ResourceType]. Called when a query returns a [ResourceType]. */
  @JvmStatic
  @TypeConverter
  fun stringToResourceType(data: String) =
    resourceTypeLookup[data] ?: throw IllegalArgumentException("invalid resource type: $data")

  @JvmStatic @TypeConverter fun bigDecimalToString(value: BigDecimal): String = value.toString()

  @JvmStatic @TypeConverter fun stringToBigDecimal(value: String): BigDecimal = value.toBigDecimal()

  @JvmStatic
  @TypeConverter
  fun temporalPrecisionToInt(temporalPrecision: TemporalPrecisionEnum): Int =
    temporalPrecision.calendarConstant

  @JvmStatic
  @TypeConverter
  fun intToTemporalPrecision(intTp: Int): TemporalPrecisionEnum {
    return when (intTp) {
      Calendar.YEAR -> TemporalPrecisionEnum.YEAR
      Calendar.MONTH -> TemporalPrecisionEnum.MONTH
      Calendar.DATE -> TemporalPrecisionEnum.DAY
      Calendar.MINUTE -> TemporalPrecisionEnum.MINUTE
      Calendar.SECOND -> TemporalPrecisionEnum.SECOND
      Calendar.MILLISECOND -> TemporalPrecisionEnum.MILLI
      else -> throw IllegalArgumentException("Unknown TemporalPrecision int $intTp")
    }
  }

  @JvmStatic
  @TypeConverter
  fun localChangeTypeToInt(updateType: LocalChangeEntity.Type): Int = updateType.value

  @JvmStatic
  @TypeConverter
  fun intToLocalChangeType(value: Int): LocalChangeEntity.Type = LocalChangeEntity.Type.from(value)
}

/*
 * 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
 *
 *       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.android.fhir.db.impl

import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.google.android.fhir.db.impl.dao.LocalChangeDao
import com.google.android.fhir.db.impl.dao.ResourceDao
import com.google.android.fhir.db.impl.dao.SyncedResourceDao
import com.google.android.fhir.db.impl.entities.DateIndexEntity
import com.google.android.fhir.db.impl.entities.LocalChangeEntity
import com.google.android.fhir.db.impl.entities.NumberIndexEntity
import com.google.android.fhir.db.impl.entities.QuantityIndexEntity
import com.google.android.fhir.db.impl.entities.ReferenceIndexEntity
import com.google.android.fhir.db.impl.entities.ResourceEntity
import com.google.android.fhir.db.impl.entities.StringIndexEntity
import com.google.android.fhir.db.impl.entities.SyncedResourceEntity
import com.google.android.fhir.db.impl.entities.TokenIndexEntity
import com.google.android.fhir.db.impl.entities.UriIndexEntity

@Database(
  entities =
    [
      ResourceEntity::class,
      StringIndexEntity::class,
      ReferenceIndexEntity::class,
      TokenIndexEntity::class,
      QuantityIndexEntity::class,
      UriIndexEntity::class,
      DateIndexEntity::class,
      NumberIndexEntity::class,
      SyncedResourceEntity::class,
      LocalChangeEntity::class],
  version = 1,
  exportSchema = false
)
@TypeConverters(DbTypeConverters::class)
internal abstract class ResourceDatabase : RoomDatabase() {
  abstract fun resourceDao(): ResourceDao
  abstract fun syncedResourceDao(): SyncedResourceDao
  abstract fun localChangeDao(): LocalChangeDao
}

/*
 * 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
 *
 *       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.android.fhir.index.entities

import ca.uhn.fhir.model.api.TemporalPrecisionEnum

/**
 * An index record for a date value in a resource.
 *
 * See https://hl7.org/FHIR/search.html#date.
 */
internal data class DateIndex(
  /** The name of the date index, e.g. "birthdate". */
  val name: String,
  /** The path of the date index, e.g. "Patient.birthdate". */
  val path: String,
  /** The high timestamp. */
  val tsHigh: Long,
  /** The low timestamp. */
  val tsLow: Long,
  /**
   * The smallest value we can unambiguously resolve the date to. This is an indication to clients
   * that any part of the timestamp smaller than the [temporalPrecision] should be ignored.
   */
  val temporalPrecision: TemporalPrecisionEnum
)

/*
 * 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
 *
 *       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.android.fhir.index.entities

import java.math.BigDecimal

/**
 * An index record for a number value in a resource.
 *
 * See https://hl7.org/FHIR/search.html#number.
 */
internal data class NumberIndex(
  /** The name of the number index, e.g. "probability". */
  val name: String,
  /** The path of the number index, e.g. "RiskAssessment.prediction.probability". */
  val path: String,
  /** The value of the number index, e.g. "0.1". */
  val value: BigDecimal
)

/*
 * 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
 *
 *       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.android.fhir.index.entities

import java.math.BigDecimal

/**
 * An index record for a quantity value in a resource.
 *
 * See https://hl7.org/FHIR/search.html#quantity.
 */
internal data class QuantityIndex(
  val name: String,
  val path: String,
  val system: String,
  val unit: String,
  val value: BigDecimal
)

/*
 * 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
 *
 *       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.android.fhir.index.entities

/**
 * An index record for a reference value in a resource.
 *
 * See https://hl7.org/FHIR/search.html#reference.
 */
internal data class ReferenceIndex(
  /** The name of the reference index, e.g. "subject". */
  val name: String,
  /** The path of the reference index, e.g. "Observation.subject". */
  val path: String,
  /** The value of the reference index, e.g. "Patient/123". */
  val value: String
)

/*
 * 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
 *
 *       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.android.fhir.index.entities

/**
 * An index record for a string value in a resource.
 *
 * See https://hl7.org/FHIR/search.html#string.
 */
internal data class StringIndex(
  /** The name of the string index, e.g. "given". */
  val name: String,
  /** The path of the string index, e.g. "Patient.name.given". */
  val path: String,
  /** The value of the string index, e.g. "Tom". */
  val value: String
)

/*
 * 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
 *
 *       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.android.fhir.index.entities

/**
 * An index record for a token value in a resource.
 *
 * See https://hl7.org/FHIR/search.html#token.
 */
internal data class TokenIndex(
  /** The name of the code index, e.g. "code". */
  val name: String,
  /** The path of the code index, e.g. "Observation.code". */
  val path: String,
  /** The system of the code index, e.g. "http://openmrs.org/concepts". */
  val system: String?,
  /** The value of the code index, e.g. "1427AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA". */
  val value: String
)

/*
 * 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
 *
 *       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.android.fhir.index.entities

/**
 * An index record for a URI value in a resource.
 *
 * See https://hl7.org/FHIR/search.html#uri.
 */
internal data class UriIndex(val name: String, val path: String, val uri: String)

/*
 * 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
 *
 *       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.android.fhir.search.filter

import com.google.android.fhir.search.impl.ResourceIdQuery
import org.hl7.fhir.r4.model.Resource

/** [FilterCriterion] that is satisfied if all of the sub [FilterCriterion] s are satisfied. */
class AndFilterCriterion constructor(val left: FilterCriterion, val right: FilterCriterion) :
  FilterCriterion {
  override fun <R : Resource> query(clazz: Class<R>): ResourceIdQuery {
    val leftQuery = left.query(clazz)
    val rightQuery = right.query(clazz)
    return ResourceIdQuery(
      "${leftQuery.query} INTERSECT ${rightQuery.query}",
      leftQuery.args + rightQuery.args
    )
  }
}

fun and(left: FilterCriterion, right: FilterCriterion) = AndFilterCriterion(left, right)

/*
 * 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
 *
 *       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.android.fhir.search.filter

import com.google.android.fhir.search.impl.ResourceIdQuery
import org.hl7.fhir.r4.model.Resource

/** Interface to specify filtering criteria for search. */
interface FilterCriterion {
  fun and(filterCriterion: FilterCriterion): FilterCriterion = and(this, filterCriterion)

  fun or(filterCriterion: FilterCriterion): FilterCriterion = or(this, filterCriterion)

  /** Returns the [ResourceIdQuery] that can be used to construct a query. */
  fun <R : Resource> query(clazz: Class<R>): ResourceIdQuery
}

/*
 * 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
 *
 *       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.android.fhir.search.filter

import com.google.android.fhir.search.impl.ResourceIdQuery
import org.hl7.fhir.r4.model.Resource

/** [FilterCriterion] that is satisfied if any of the sub [FilterCriterion] s is satisfied. */
class OrFilterCriterion constructor(val left: FilterCriterion, val right: FilterCriterion) :
  FilterCriterion {
  override fun <R : Resource> query(clazz: Class<R>): ResourceIdQuery {
    val leftQuery = left.query(clazz)
    val rightQuery = right.query(clazz)
    return ResourceIdQuery(
      "${leftQuery.query} UNION ${rightQuery.query}",
      leftQuery.args + rightQuery.args
    )
  }
}

fun or(left: FilterCriterion, right: FilterCriterion) = OrFilterCriterion(left, right)

/*
 * 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
 *
 *       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.android.fhir.search.filter

import ca.uhn.fhir.rest.gclient.ReferenceClientParam
import com.google.android.fhir.search.impl.ResourceIdQuery
import org.hl7.fhir.r4.model.Resource

/** [FilterCriterion] on a reference value. */
class ReferenceFilterCriterion constructor(val param: ReferenceClientParam, val value: String) :
  FilterCriterion {
  override fun <R : Resource> query(clazz: Class<R>): ResourceIdQuery {
    // TODO: implement different queries for different operators.
    return ResourceIdQuery(
      """
            SELECT resourceId FROM ReferenceIndexEntity
            WHERE resourceType = ? AND index_name = ? AND index_value = ?""".trimIndent(),
      listOf(clazz.simpleName, param.paramName, value)
    )
  }
}

fun reference(param: ReferenceClientParam, value: String) = ReferenceFilterCriterion(param, value)

/*
 * 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
 *
 *       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.android.fhir.search.filter

import ca.uhn.fhir.rest.gclient.StringClientParam
import ca.uhn.fhir.rest.param.ParamPrefixEnum
import com.google.android.fhir.search.impl.ResourceIdQuery
import org.hl7.fhir.r4.model.Resource

/**
 * [FilterCriterion] on a string value.
 *
 * For example:
 * * name that matches 'Tom'
 * * address that includes 'London'
 */
class StringFilteringCriterion
constructor(val param: StringClientParam, val operator: ParamPrefixEnum, val value: String) :
  FilterCriterion {
  override fun <R : Resource> query(clazz: Class<R>): ResourceIdQuery {
    // TODO: implement different queries for different operators.
    return ResourceIdQuery(
      """
SELECT resourceId FROM StringIndexEntity
WHERE resourceType = ? AND index_name = ? AND index_value = ?""",
      listOf(clazz.simpleName, param.paramName, value)
    )
  }
}

fun string(param: StringClientParam, operator: ParamPrefixEnum, value: String) =
  StringFilteringCriterion(param, operator, value)

/*
 * 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
 *
 *       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.android.fhir.search.impl

import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery

/** Query that returns a list of resource IDs. */
abstract class Query {
  abstract fun getQueryString(): String
  abstract fun getQueryArgs(): List<Any>
  fun getSupportSQLiteQuery(): SupportSQLiteQuery =
    SimpleSQLiteQuery(getQueryString(), getQueryArgs().toTypedArray())
}

/*
 * 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
 *
 *       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.android.fhir.search.impl

/** Query that returns a list of resource IDs. */
data class ResourceIdQuery(val query: String, val args: List<Any>) : Query() {
  override fun getQueryString(): String = query
  override fun getQueryArgs(): List<Any> = args
}

/*
 * 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
 *
 *       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.android.fhir.search.impl

import com.google.android.fhir.db.Database
import com.google.android.fhir.resource.getResourceType
import com.google.android.fhir.search.Search
import com.google.android.fhir.search.filter.FilterCriterion
import com.google.android.fhir.search.sort.SortCriterion
import org.hl7.fhir.r4.model.Resource

/** Implementation of the [Search] interface. */
class SearchImpl constructor(val database: Database) : Search {
  override fun <R : Resource> of(clazz: Class<R>) = SearchSpecificationImpl(clazz)

  /** Implementation of the [Search.SearchSpecifications] interface. */
  inner class SearchSpecificationImpl<R : Resource>(val clazz: Class<R>) :
    Search.SearchSpecifications {
    private var filterCriterion: FilterCriterion? = null
    private var sortCriterion: SortCriterion? = null
    private var limit: Int? = null
    private var skip: Int? = null

    override fun filter(filterCriterion: FilterCriterion): Search.SearchSpecifications = apply {
      this.filterCriterion = filterCriterion
    }

    override fun sort(sortCriterion: SortCriterion): Search.SearchSpecifications = apply {
      this.sortCriterion = sortCriterion
    }

    override fun limit(limit: Int): Search.SearchSpecifications = apply { this.limit = limit }

    override fun skip(skip: Int): Search.SearchSpecifications = apply { this.skip = skip }

    override fun <R : Resource> run(): List<R> =
      database.search(
        SerializedResourceQuery(
          getResourceType(clazz),
          filterCriterion?.query(clazz),
          sortCriterion,
          limit,
          skip
        )
      )
  }
}

/*
 * 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
 *
 *       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.android.fhir.search.impl

import com.google.android.fhir.search.sort.SortCriterion
import org.hl7.fhir.r4.model.ResourceType

/** Query that returns a list of serialized resources. */
data class SerializedResourceQuery(
  val resourceType: ResourceType,
  val resourceIdQuery: ResourceIdQuery?,
  val sortCriterion: SortCriterion?,
  val limit: Int?,
  val skip: Int?
) : Query() {
  override fun getQueryString(): String {
    val queryBuilder = StringBuilder()
    queryBuilder.appendln(
      """
            SELECT a.serializedResource
            FROM ResourceEntity a
        """.trimIndent()
    )
    sortCriterion?.also {
      queryBuilder.appendln(
        """
                LEFT JOIN ${sortCriterion.table} b
                ON a.resourceType = b.resourceType AND a.resourceId = b.resourceId AND b.index_name = ?
            """.trimIndent()
      )
    }
    queryBuilder.appendln(
      """
            WHERE a.resourceType = ?${resourceIdQuery?.let { " AND a.resourceId IN (${it.query})" } ?: ""}
        """.trimIndent()
    )
    sortCriterion?.also {
      queryBuilder.appendln(
        """
                    ORDER BY b.index_value ${
                        if (sortCriterion.ascending) {
                            "ASC"
                        } else {
                            "DESC"
                        }
                    }
                """.trimIndent()
      )
    }
    limit?.also {
      queryBuilder.appendln(
        """
                LIMIT ?${skip?.let { " OFFSET ?" } ?: ""}
            """.trimIndent()
      )
    }
    return queryBuilder.toString().trimIndent()
  }

  override fun getQueryArgs(): List<Any> {
    var list: List<Any> =
      if (sortCriterion == null) {
        listOf()
      } else {
        listOf(sortCriterion.param)
      }
    list = list + resourceType.name
    resourceIdQuery?.also { list = list + it.getQueryArgs() }
    limit?.also {
      list = list + it
      skip?.also { list = list + it }
    }
    return list
  }
}

/*
 * 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
 *
 *       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.android.fhir.search.sort

/** Interface to specify filtering criteria for search. */
interface SortCriterion {
  val table: String
  val param: String
  val ascending: Boolean
}

/*
 * 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
 *
 *       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.android.fhir.search.sort

import ca.uhn.fhir.rest.gclient.StringClientParam
import com.google.android.fhir.search.filter.FilterCriterion

/**
 * [FilterCriterion] on a string value.
 *
 * For example:
 * * name that matches 'Tom'
 * * address that includes 'London'
 */
data class StringSortCriterion
constructor(val stringParam: StringClientParam, override val ascending: Boolean) : SortCriterion {
  override val table: String = "StringIndexEntity"
  override val param = stringParam.paramName
}

fun stringSort(param: StringClientParam, ascending: Boolean) = StringSortCriterion(param, ascending)

/*
 * 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
 *
 *       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.android.fhir.cqlreference.cql

import org.hl7.fhir.instance.model.api.IBase
import org.opencds.cqf.cql.model.R4FhirModelResolver

/** An [R4FhirModelResolver] on Android. */
internal class AndroidR4FhirModelResolver : R4FhirModelResolver() {
  override fun getContextPath(contextType: String?, targetType: String?): Any? {
    var targetType: String? = targetType
    if (targetType == null || contextType == null) {
      return null
    }
    when (contextType) {
      "Unspecified", "Population" -> return null
      targetType -> return "id"
    }

    // Workaround for the issue of target type incorrectly including a namespace URI prefix.
    // TODO: remove this.
    if (targetType.startsWith(NAMESPACE_URI_PREFIX)) {
      targetType = targetType.substring(NAMESPACE_URI_PREFIX.length)
    }

    val resourceDefinition = fhirContext.getResourceDefinition(targetType)
    val theValue = this.createInstance(contextType)
    val type = theValue.javaClass as Class<out IBase?>
    val children = resourceDefinition.children
    return children
      .asSequence()
      .mapNotNull { child -> innerGetContextPath(child, type) }
      .firstOrNull()
  }

  companion object {
    /**
     * A prefix that is incorrectly included on Android due to the inconsistency of JSON
     * deserialization.
     */
    const val NAMESPACE_URI_PREFIX = "{http://hl7.org/fhir}"
  }
}

/*
 * 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
 *
 *       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.android.fhir.cqlreference.cql

import android.content.Context

class CqlEngineUtils(private val context: Context) {

  class Factory() {
    fun createCqlEngine(context: Context) {
      // TODO: Not implemented
    }
  }

  /** Returns the result of a CQL evaluation provided with the ID of the library. */
  fun evaluateCql(libraryVersionId: String, contextString: String, expression: String) {
    // TODO Not implemented
  }
}

/*
 * 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
 *
 *       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.android.fhir.cqlreference.cql

import org.opencds.cqf.cql.data.CompositeDataProvider
import org.opencds.cqf.cql.model.ModelResolver
import org.opencds.cqf.cql.retrieve.RetrieveProvider

/**
 * FHIR Engine's implementation of a [org.opencds.cqf.cql.data.DataProvider] which provides the
 * [org.opencds.cqf.cql.execution.CqlEngine] required data to complete CQL evaluation.
 */
internal class FhirEngineDataProvider
internal constructor(modelResolver: ModelResolver, retrieveProvider: RetrieveProvider) :
  CompositeDataProvider(modelResolver, retrieveProvider)

/*
 * 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
 *
 *       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.android.fhir.cqlreference.cql

import org.opencds.cqf.cql.runtime.Code
import org.opencds.cqf.cql.terminology.CodeSystemInfo
import org.opencds.cqf.cql.terminology.TerminologyProvider
import org.opencds.cqf.cql.terminology.ValueSetInfo

/** Fhir Engine's implementation of [TerminologyProvider]. */
internal class FhirEngineTerminologyProvider : TerminologyProvider {
  override fun `in`(code: Code, valueSet: ValueSetInfo): Boolean = false

  override fun expand(valueSet: ValueSetInfo): Iterable<Code>? = null

  override fun lookup(code: Code, codeSystem: CodeSystemInfo): Code? = null
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.enablement

import java.lang.IllegalStateException
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse

/**
 * Evaluator for the enablement status of a [Questionnaire.QuestionnaireItemComponent]. Uses the
 * `enableWhen` constraints and the `enableBehavior` value defined in the
 * [Questionnaire.QuestionnaireItemComponent]. Also depends on the answers (or lack thereof)
 * captured in the specified [QuestionnaireResponse.QuestionnaireResponseItemComponent] s.
 *
 * For example, the following `enableWhen` constraint in a
 * [Questionnaire.QuestionnaireItemComponent]
 * ```
 *     "enableWhen": [
 *       {
 *         "question": "vitaminKgiven",
 *         "operator": "exists",
 *         "answerBoolean": true
 *       }
 *     ],
 * ```
 * specifies that the [Questionnaire.QuestionnaireItemComponent] should be enabled only if the
 * question with ID `vitaminKgiven` has been answered.
 *
 * The enablement status typically determines whether the [Questionnaire.QuestionnaireItemComponent]
 * is shown or hidden. However, it is also possible that only user interaction is enabled or
 * disabled (e.g. grayed out) with the [Questionnaire.QuestionnaireItemComponent] always shown.
 *
 * For more information see
 * [Questionnaire.item.enableWhen](https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.enableWhen)
 * and
 * [Questionnaire.item.enableBehavior](https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.enableBehavior)
 * .
 */
internal object EnablementEvaluator {

  /**
   * Returns whether [questionnaireItem] should be enabled.
   *
   * @param questionnaireResponseItemRetriever function that returns the
   * [QuestionnaireResponse.Item] with the `linkId`, or null if there isn't one.
   *
   * For example, the questionnaireItem might be
   */
  fun evaluate(
    questionnaireItem: Questionnaire.QuestionnaireItemComponent,
    questionnaireItemAndQuestionnaireResponseItemRetriever:
      (linkId: String) -> QuestionnaireItemWithResponse
  ): Boolean {
    val enableWhenList = questionnaireItem.enableWhen

    // The questionnaire item is enabled by default if there is no `enableWhen` constraint.
    if (enableWhenList.isEmpty()) return true

    // Evaluate single `enableWhen` constraint.
    if (enableWhenList.size == 1) {
      return evaluateEnableWhen(
        enableWhenList.single(),
        questionnaireItemAndQuestionnaireResponseItemRetriever
      )
    }

    // Evaluate multiple `enableWhen` constraints and aggregate the results according to
    // `enableBehavior` which specifies one of the two behaviors: 1) the questionnaire item is
    // enabled if ALL `enableWhen` constraints are satisfied, or 2) the questionnaire item is
    // enabled if ANY `enableWhen` constraint is satisfied.
    return when (val value = questionnaireItem.enableBehavior) {
      Questionnaire.EnableWhenBehavior.ALL ->
        enableWhenList.all {
          evaluateEnableWhen(it, questionnaireItemAndQuestionnaireResponseItemRetriever)
        }
      Questionnaire.EnableWhenBehavior.ANY ->
        enableWhenList.any {
          evaluateEnableWhen(it, questionnaireItemAndQuestionnaireResponseItemRetriever)
        }
      else -> throw IllegalStateException("Unrecognized enable when behavior $value")
    }
  }
}

/** Result class to unpack questionnaireItem and questionnaireResponseItem */
data class QuestionnaireItemWithResponse(
  val questionnaireItem: Questionnaire.QuestionnaireItemComponent?,
  val questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent?
)

/**
 * Returns whether the `enableWhen` constraint is satisfied.
 *
 * @param questionnaireResponseItemRetriever function that returns the [QuestionnaireResponse.Item]
 * with the `linkId`, or null if there isn't one.
 */
private fun evaluateEnableWhen(
  enableWhen: Questionnaire.QuestionnaireItemEnableWhenComponent,
  questionnaireResponseItemRetriever: (linkId: String) -> QuestionnaireItemWithResponse
): Boolean {
  val (questionnaireItem, questionnaireResponseItem) =
    questionnaireResponseItemRetriever(enableWhen.question)
  if (questionnaireItem == null || questionnaireResponseItem == null) return true
  return if (Questionnaire.QuestionnaireItemOperator.EXISTS == enableWhen.operator) {
    (questionnaireResponseItem.answer.size > 0) == enableWhen.answerBooleanType.booleanValue()
  } else {
    questionnaireResponseItem.contains(
      enableWhenTypeToPredicate(enableWhen, questionnaireItem.type)
    )
  }
}

/**
 * Return if any answer in the answer list satisfies the passed predicate.
 *
 * @param predicate boolean predicate function that takes a [QuestionnaireResponse.Item.Answer].
 */
private fun QuestionnaireResponse.QuestionnaireResponseItemComponent.contains(
  predicate: (QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent) -> Boolean
): Boolean {
  return this.answer.any { predicate(it) }
}

/**
 * Returns a predicate based on the `EnableWhen` `operator` and `Answer` value.
 *
 * @param type used to get value based on [Questionnaire.Item.TypeCode].
 */
private fun enableWhenTypeToPredicate(
  enableWhen: Questionnaire.QuestionnaireItemEnableWhenComponent,
  type: Questionnaire.QuestionnaireItemType
): (QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent) -> Boolean {
  when (val operator = enableWhen.operator) {
    Questionnaire.QuestionnaireItemOperator.EQUAL -> return {
        it.value.toString() == enableWhen.answer.toString()
      }
    Questionnaire.QuestionnaireItemOperator.NOT_EQUAL -> return {
        it.value.toString() != enableWhen.answer.toString()
      }
    else -> throw NotImplementedError("Enable when operator $operator is not implemented.")
  }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.mapping

import com.google.android.fhir.datacapture.getValueForType
import java.util.Locale
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.DecimalType
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.IntegerType
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.StringType
import org.hl7.fhir.r4.model.TimeType
import org.hl7.fhir.r4.model.UrlType

/**
 * Maps [QuestionnaireResponse] s to FHIR resources and vice versa.
 *
 * The process of converting [QuestionnaireResponse] s to other FHIR resources is called
 * [extraction](http://build.fhir.org/ig/HL7/sdc/extraction.html). The reverse process of converting
 * existing FHIR resources to [QuestionnaireResponse] s to be used to pre-fill the UI is called
 * [population](http://build.fhir.org/ig/HL7/sdc/populate.html).
 *
 * [Definition-based extraction](http://build.fhir.org/ig/HL7/sdc/extraction.html#definition-based-extraction)
 * and
 * [expression-based population](http://build.fhir.org/ig/HL7/sdc/populate.html#expression-based-population)
 * are used because these approaches are generic enough to work with any FHIR resource types, and at
 * the same time relatively easy to implement.
 *
 * WARNING: This is not production-ready.
 */
internal object ResourceMapper {

  /**
   * Extract a FHIR resource from the `questionnaire` and `questionnaireResponse`.
   *
   * This method assumes there is only one FHIR resource to be extracted from the given
   * `questionnaire` and `questionnaireResponse`.
   */
  fun extract(questionnaire: Questionnaire, questionnaireResponse: QuestionnaireResponse): Base {
    return questionnaire
      .itemContextNameToExpressionMap
      .values
      .first()
      .let { Class.forName("org.hl7.fhir.r4.model.$it").newInstance() as Base }
      .apply { extractFields(questionnaire.item, questionnaireResponse.item) }
  }
}

/**
 * Extracts values for fields in the builder from the corresponding questions and answers in
 * [questionnaireItemList] and [questionnaireResponseItemList].
 */
private fun Base.extractFields(
  questionnaireItemList: List<Questionnaire.QuestionnaireItemComponent>,
  questionnaireResponseItemList: List<QuestionnaireResponse.QuestionnaireResponseItemComponent>
) {
  val questionnaireItemListIterator = questionnaireItemList.iterator()
  val questionnaireResponseItemListIterator = questionnaireResponseItemList.iterator()
  while (questionnaireItemListIterator.hasNext() &&
    questionnaireResponseItemListIterator.hasNext()) {
    val questionnaireItem = questionnaireItemListIterator.next()
    val questionnaireResponseItem = questionnaireResponseItemListIterator.next()
    this.extractField(questionnaireItem, questionnaireResponseItem)
    extractFields(questionnaireItem.item, questionnaireResponseItem.item)
  }
}

/**
 * Extracts value for field in the builder from the corresponding question and answer in
 * [questionnaireItem] and [questionnaireResponseItem].
 *
 * NOTE: Nested fields are not handled. See https://github.com/google/android-fhir/issues/240.
 */
private fun Base.extractField(
  questionnaireItem: Questionnaire.QuestionnaireItemComponent,
  questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent
) {
  val targetFieldName = questionnaireItem.definitionFieldName ?: return
  if (targetFieldName.isEmpty()) {
    return
  }

  questionnaireItem.type.getClass()?.let {
    this.javaClass
      .getMethod("set${targetFieldName.capitalize(Locale.ROOT)}Element", it)
      .invoke(
        this,
        questionnaireResponseItem.answer.single().getValueForType(questionnaireItem.type)
      )
  }
}

/**
 * Returns the field name for the [Questionnaire.Item]'s definition.
 *
 * For example, if the definition URI is
 * `"http://hl7.org/fhir/StructureDefinition/Patient#Patient.birthDate"`, this function will return
 * `"birthDate"`.
 */
private val Questionnaire.QuestionnaireItemComponent.definitionFieldName
  get() = this.definition?.substringAfterLast(".")

/**
 * Returns the [Class] for the answer to the [Questionnaire.QuestionnaireItemComponent].
 *
 * Used to retrieve the method to invoke to set the field in the extracted FHIR resource.
 */
private fun Questionnaire.QuestionnaireItemType.getClass(): Class<out Base>? =
  when (this) {
    Questionnaire.QuestionnaireItemType.DATE -> DateType::class.java
    Questionnaire.QuestionnaireItemType.BOOLEAN -> BooleanType::class.java
    Questionnaire.QuestionnaireItemType.DECIMAL -> DecimalType::class.java
    Questionnaire.QuestionnaireItemType.INTEGER -> IntegerType::class.java
    Questionnaire.QuestionnaireItemType.DATETIME -> DateTimeType::class.java
    Questionnaire.QuestionnaireItemType.TIME -> TimeType::class.java
    Questionnaire.QuestionnaireItemType.STRING, Questionnaire.QuestionnaireItemType.TEXT ->
      StringType::class.java
    Questionnaire.QuestionnaireItemType.URL -> UrlType::class.java
    else -> null
  }

/**
 * The map from the `name`s to `expression`s in the
 * [item extraction context extension](http://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-itemExtractionContext.html)
 * s.
 */
private val Questionnaire.itemContextNameToExpressionMap: Map<String, String>
  get() {
    return this.extension
      .filter { it.url == ITEM_CONTEXT_EXTENSION_URL }
      .map {
        val expression = it.value as Expression
        expression.name to expression.expression
      }
      .toMap()
  }

/**
 * See
 * [Extension: item extraction context](http://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-itemExtractionContext.html)
 * .
 */
private const val ITEM_CONTEXT_EXTENSION_URL: String =
  "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemContext"

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.annotation.SuppressLint
import android.app.DatePickerDialog
import android.app.Dialog
import android.os.Bundle
import android.widget.DatePicker
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.setFragmentResult
import java.time.LocalDate

internal class DatePickerFragment : DialogFragment(), DatePickerDialog.OnDateSetListener {
  @SuppressLint("NewApi") // Suppress warnings for java.time APIs
  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    // Use the current date as the default date in the picker
    val today = LocalDate.now()

    // Create a new instance of DatePickerDialog and return it
    return DatePickerDialog(
      requireContext(),
      this,
      today.year,
      // month values are 1-12 in java.time but DatePickerDialog expects 0-11
      today.monthValue - 1,
      today.dayOfMonth
    )
  }

  override fun onDateSet(view: DatePicker?, year: Int, month: Int, dayOfMonth: Int) {
    setFragmentResult(
      RESULT_REQUEST_KEY,
      bundleOf(
        RESULT_BUNDLE_KEY_YEAR to year,
        RESULT_BUNDLE_KEY_MONTH to month,
        RESULT_BUNDLE_KEY_DAY_OF_MONTH to dayOfMonth
      )
    )
    dismiss()
  }

  companion object {
    const val TAG = "date-picker-fragment"
    const val RESULT_REQUEST_KEY = "date-picker-request-key"
    const val RESULT_BUNDLE_KEY_YEAR = "date-picker-bundle-key-year"
    const val RESULT_BUNDLE_KEY_MONTH = "date-picker-bundle-key-month"
    const val RESULT_BUNDLE_KEY_DAY_OF_MONTH = "date-picker-bundle-day-of-month"
  }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.view.View
import android.widget.CheckBox
import android.widget.TextView
import com.google.android.fhir.datacapture.R
import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.QuestionnaireResponse

internal object QuestionnaireItemCheckBoxViewHolderFactory :
  QuestionnaireItemViewHolderFactory(R.layout.questionnaire_item_check_box_view) {
  override fun getQuestionnaireItemViewHolderDelegate() =
    object : QuestionnaireItemViewHolderDelegate {
      private lateinit var prefixTextView: TextView
      private lateinit var checkBox: CheckBox
      private lateinit var questionnaireItemViewItem: QuestionnaireItemViewItem

      override fun init(itemView: View) {
        prefixTextView = itemView.findViewById(R.id.prefix)
        checkBox = itemView.findViewById(R.id.check_box)
        checkBox.setOnClickListener {
          // if-else block to prevent over-writing of "items" nested within "answer"
          if (questionnaireItemViewItem.singleAnswerOrNull != null) {
            questionnaireItemViewItem.singleAnswerOrNull!!.value = BooleanType(checkBox.isChecked)
          } else {
            questionnaireItemViewItem.singleAnswerOrNull =
              QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
                value = BooleanType(checkBox.isChecked)
              }
          }

          questionnaireItemViewItem.questionnaireResponseItemChangedCallback()
        }
      }

      override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) {
        this.questionnaireItemViewItem = questionnaireItemViewItem
        if (!questionnaireItemViewItem.questionnaireItem.prefix.isNullOrEmpty()) {
          prefixTextView.visibility = View.VISIBLE
          prefixTextView.text = questionnaireItemViewItem.questionnaireItem.prefix
        } else {
          prefixTextView.visibility = View.GONE
        }
        checkBox.text = questionnaireItemViewItem.questionnaireItem.text
        checkBox.isChecked =
          questionnaireItemViewItem.singleAnswerOrNull?.valueBooleanType?.value ?: false
      }
    }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ContextThemeWrapper
import androidx.fragment.app.FragmentResultListener
import com.google.android.fhir.datacapture.R
import com.google.android.material.textfield.TextInputEditText
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.QuestionnaireResponse

internal object QuestionnaireItemDatePickerViewHolderFactory :
  QuestionnaireItemViewHolderFactory(R.layout.questionnaire_item_date_picker_view) {
  override fun getQuestionnaireItemViewHolderDelegate() =
    object : QuestionnaireItemViewHolderDelegate {
      private lateinit var prefixTextView: TextView
      private lateinit var textDateQuestion: TextView
      private lateinit var textInputEditText: TextInputEditText
      private lateinit var questionnaireItemViewItem: QuestionnaireItemViewItem

      override fun init(itemView: View) {
        prefixTextView = itemView.findViewById(R.id.prefix)
        textDateQuestion = itemView.findViewById(R.id.question)
        textInputEditText = itemView.findViewById(R.id.textInputEditText)
        // Disable direct text input to only allow input from the date picker dialog
        textInputEditText.keyListener = null
        textInputEditText.setOnFocusChangeListener { _: View, hasFocus: Boolean ->
          // Do not show the date picker dialog when losing focus.
          if (!hasFocus) return@setOnFocusChangeListener

          // The application is wrapped in a ContextThemeWrapper in QuestionnaireFragment
          // and again in TextInputEditText during layout inflation. As a result, it is
          // necessary to access the base context twice to retrieve the application object
          // from the view's context.
          val context = itemView.context.tryUnwrapContext()!!
          context.supportFragmentManager.setFragmentResultListener(
            DatePickerFragment.RESULT_REQUEST_KEY,
            context,
            object : FragmentResultListener {
              // java.time APIs can be used with desugaring
              @SuppressLint("NewApi")
              override fun onFragmentResult(requestKey: String, result: Bundle) {
                val year = result.getInt(DatePickerFragment.RESULT_BUNDLE_KEY_YEAR)
                val month = result.getInt(DatePickerFragment.RESULT_BUNDLE_KEY_MONTH)
                val dayOfMonth = result.getInt(DatePickerFragment.RESULT_BUNDLE_KEY_DAY_OF_MONTH)
                textInputEditText.setText(
                  LocalDate.of(
                      year,
                      // Month values are 1-12 in java.time but 0-11 in
                      // DatePickerDialog.
                      month + 1,
                      dayOfMonth
                    )
                    .format(LOCAL_DATE_FORMATTER)
                )

                val date = DateType(year, month, dayOfMonth)
                questionnaireItemViewItem.singleAnswerOrNull =
                  QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
                    value = date
                  }
                questionnaireItemViewItem.questionnaireResponseItemChangedCallback()
              }
            }
          )
          DatePickerFragment().show(context.supportFragmentManager, DatePickerFragment.TAG)
          // Clear focus so that the user can refocus to open the dialog
          textDateQuestion.clearFocus()
        }
      }

      @SuppressLint("NewApi") // java.time APIs can be used due to desugaring
      override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) {
        this.questionnaireItemViewItem = questionnaireItemViewItem
        if (!questionnaireItemViewItem.questionnaireItem.prefix.isNullOrEmpty()) {
          prefixTextView.visibility = View.VISIBLE
          prefixTextView.text = questionnaireItemViewItem.questionnaireItem.prefix
        } else {
          prefixTextView.visibility = View.GONE
        }
        textDateQuestion.text = questionnaireItemViewItem.questionnaireItem.text
        textInputEditText.setText(
          questionnaireItemViewItem
            .singleAnswerOrNull
            ?.valueDateType
            ?.let {
              LocalDate.of(
                it.year,
                // month values are 1-12 in java.time but 0-11 in DateType (FHIR)
                it.month + 1,
                it.day
              )
            }
            ?.format(LOCAL_DATE_FORMATTER)
            ?: ""
        )
      }
    }

  @SuppressLint("NewApi") // java.time APIs can be used due to desugaring
  val LOCAL_DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE!!
}

const val NUMBER_OF_MICROSECONDS_PER_SECOND = 1000000
const val NUMBER_OF_MICROSECONDS_PER_MILLISECOND = 1000

/**
 * Returns the [AppCompatActivity] if there exists one wrapped inside [ContextThemeWrapper] s, or
 * `null` otherwise.
 *
 * This function is inspired by the function with the same name in `AppCompateDelegateImpl`. See
 * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java;l=1615
 *
 * TODO: find a more robust way to do this as it is not guaranteed that the activity is an
 * AppCompatActivity.
 */
internal fun Context.tryUnwrapContext(): AppCompatActivity? {
  var context = this
  while (true) {
    when (context) {
      is AppCompatActivity -> return context
      is ContextThemeWrapper -> context = context.baseContext
      else -> return null
    }
  }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.annotation.SuppressLint
import android.view.View
import android.widget.TextView
import com.google.android.fhir.datacapture.R
import com.google.android.material.textfield.TextInputEditText
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Date
import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.QuestionnaireResponse

internal object QuestionnaireItemDateTimePickerViewHolderFactory :
  QuestionnaireItemViewHolderFactory(R.layout.questionnaire_item_date_time_picker_view) {
  override fun getQuestionnaireItemViewHolderDelegate() =
    object : QuestionnaireItemViewHolderDelegate {
      private lateinit var prefixTextView: TextView
      private lateinit var textDateQuestion: TextView
      private lateinit var dateInputEditText: TextInputEditText
      private lateinit var textTimeQuestion: TextView
      private lateinit var timeInputEditText: TextInputEditText
      private lateinit var questionnaireItemViewItem: QuestionnaireItemViewItem

      override fun init(itemView: View) {
        prefixTextView = itemView.findViewById(R.id.prefix)
        textDateQuestion = itemView.findViewById(R.id.date_question)
        dateInputEditText = itemView.findViewById(R.id.dateInputEditText)
        // Disable direct text input to only allow input from the date picker dialog
        dateInputEditText.keyListener = null
        dateInputEditText.setOnFocusChangeListener { _: View, hasFocus: Boolean ->
          // Do not show the date picker dialog when losing focus.
          if (!hasFocus) return@setOnFocusChangeListener

          // The application is wrapped in a ContextThemeWrapper in QuestionnaireFragment
          // and again in TextInputEditText during layout inflation. As a result, it is
          // necessary to access the base context twice to retrieve the application object
          // from the view's context.
          val context = itemView.context.tryUnwrapContext()!!
          context.supportFragmentManager.setFragmentResultListener(
            DatePickerFragment.RESULT_REQUEST_KEY,
            context,
            { _, result ->
              val year = result.getInt(DatePickerFragment.RESULT_BUNDLE_KEY_YEAR)
              val month = result.getInt(DatePickerFragment.RESULT_BUNDLE_KEY_MONTH)
              val dayOfMonth = result.getInt(DatePickerFragment.RESULT_BUNDLE_KEY_DAY_OF_MONTH)
              val localDateTime =
                LocalDateTime.of(
                  year,
                  // Month values are 1-12 in java.time but 0-11 in
                  // DatePickerDialog.
                  month + 1,
                  dayOfMonth,
                  0,
                  0,
                  0
                )
              updateDateTimeInput(localDateTime)
              updateDateTimeAnswer(localDateTime)
            }
          )
          DatePickerFragment().show(context.supportFragmentManager, DatePickerFragment.TAG)
          // Clear focus so that the user can refocus to open the dialog
          textDateQuestion.clearFocus()
        }

        textTimeQuestion = itemView.findViewById(R.id.time_question)
        timeInputEditText = itemView.findViewById(R.id.timeInputEditText)
        // Disable direct text input to only allow input from the time picker dialog
        timeInputEditText.keyListener = null
        timeInputEditText.setOnFocusChangeListener { _: View, hasFocus: Boolean ->
          // Do not show the date picker dialog when losing focus.
          if (!hasFocus) return@setOnFocusChangeListener

          // The application is wrapped in a ContextThemeWrapper in QuestionnaireFragment
          // and again in TextInputEditText during layout inflation. As a result, it is
          // necessary to access the base context twice to retrieve the application object
          // from the view's context.
          val context = itemView.context.tryUnwrapContext()!!
          context.supportFragmentManager.setFragmentResultListener(
            TimePickerFragment.RESULT_REQUEST_KEY,
            context,
            { _, result ->
              val hour = result.getInt(TimePickerFragment.RESULT_BUNDLE_KEY_HOUR)
              val minute = result.getInt(TimePickerFragment.RESULT_BUNDLE_KEY_MINUTE)
              val localDate = questionnaireItemViewItem.singleAnswerOrNull!!.valueDateTimeType
              val localDateTime =
                LocalDateTime.of(localDate.year, localDate.month, localDate.day, hour, minute, 0)
              updateDateTimeInput(localDateTime)
              updateDateTimeAnswer(localDateTime)
            }
          )
          TimePickerFragment().show(context.supportFragmentManager, TimePickerFragment.TAG)
          // Clear focus so that the user can refocus to open the dialog
          textTimeQuestion.clearFocus()
        }
      }

      @SuppressLint("NewApi") // java.time APIs can be used due to desugaring
      override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) {
        this.questionnaireItemViewItem = questionnaireItemViewItem
        if (!questionnaireItemViewItem.questionnaireItem.prefix.isNullOrEmpty()) {
          prefixTextView.visibility = View.VISIBLE
          prefixTextView.text = questionnaireItemViewItem.questionnaireItem.prefix
        } else {
          prefixTextView.visibility = View.GONE
        }
        textDateQuestion.text = questionnaireItemViewItem.questionnaireItem.text
        textTimeQuestion.text = questionnaireItemViewItem.questionnaireItem.text
        val dateTime = questionnaireItemViewItem.singleAnswerOrNull?.valueDateTimeType
        updateDateTimeInput(
          dateTime?.let {
            LocalDateTime.of(it.year, it.month + 1, it.day, it.hour, it.minute, it.second)
          }
        )
      }

      /** Update the date and time input fields in the UI. */
      fun updateDateTimeInput(localDateTime: LocalDateTime?) {
        timeInputEditText.isEnabled = localDateTime != null
        dateInputEditText.setText(localDateTime?.format(LOCAL_DATE_FORMATTER) ?: "")
        timeInputEditText.setText(localDateTime?.format(LOCAL_TIME_FORMATTER) ?: "")
      }

      /** Updates the recorded answer. */
      fun updateDateTimeAnswer(localDateTime: LocalDateTime) {
        questionnaireItemViewItem.singleAnswerOrNull =
          QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent()
            .setValue(
              DateTimeType(
                Date(
                  localDateTime.year - 1900,
                  localDateTime.monthValue - 1,
                  localDateTime.dayOfMonth,
                  localDateTime.hour,
                  localDateTime.minute,
                  localDateTime.second
                )
              )
            )
        questionnaireItemViewItem.questionnaireResponseItemChangedCallback()
      }
    }

  @SuppressLint("NewApi") // java.time APIs can be used due to desugaring
  val LOCAL_DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE!!
  val LOCAL_TIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_TIME!!
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.view.View
import android.widget.TextView
import com.google.android.fhir.datacapture.R

internal object QuestionnaireItemDisplayViewHolderFactory :
  QuestionnaireItemViewHolderFactory(R.layout.questionnaire_item_display_view) {
  override fun getQuestionnaireItemViewHolderDelegate() =
    object : QuestionnaireItemViewHolderDelegate {
      private lateinit var prefixTextView: TextView
      private lateinit var textView: TextView

      override fun init(itemView: View) {
        prefixTextView = itemView.findViewById(R.id.prefix)
        textView = itemView.findViewById(R.id.text_view)
      }

      override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) {
        if (!questionnaireItemViewItem.questionnaireItem.prefix.isNullOrEmpty()) {
          prefixTextView.visibility = View.VISIBLE
          prefixTextView.text = questionnaireItemViewItem.questionnaireItem.prefix
        } else {
          prefixTextView.visibility = View.GONE
        }
        textView.text = questionnaireItemViewItem.questionnaireItem.text
        textView.visibility =
          if (textView.text.isEmpty()) {
            View.GONE
          } else {
            View.VISIBLE
          }
      }
    }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.content.Context
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.TextView
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.displayString
import org.hl7.fhir.r4.model.QuestionnaireResponse

internal object QuestionnaireItemDropDownViewHolderFactory :
  QuestionnaireItemViewHolderFactory(R.layout.questionnaire_item_drop_down_view) {
  override fun getQuestionnaireItemViewHolderDelegate() =
    object : QuestionnaireItemViewHolderDelegate {
      private lateinit var prefixTextView: TextView
      private lateinit var textView: TextView
      private lateinit var autoCompleteTextView: AutoCompleteTextView
      private lateinit var questionnaireItemViewItem: QuestionnaireItemViewItem
      private lateinit var context: Context

      override fun init(itemView: View) {
        prefixTextView = itemView.findViewById(R.id.prefix)
        textView = itemView.findViewById(R.id.dropdown_question_title)
        autoCompleteTextView = itemView.findViewById(R.id.auto_complete)
        context = itemView.context
      }

      override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) {
        this.questionnaireItemViewItem = questionnaireItemViewItem
        if (!questionnaireItemViewItem.questionnaireItem.prefix.isNullOrEmpty()) {
          prefixTextView.visibility = View.VISIBLE
          prefixTextView.text = questionnaireItemViewItem.questionnaireItem.prefix
        } else {
          prefixTextView.visibility = View.GONE
        }
        textView.text = questionnaireItemViewItem.questionnaireItem.text
        val answerOptionString =
          this.questionnaireItemViewItem.questionnaireItem.answerOption.map { it.displayString }
        val adapter =
          ArrayAdapter(context, R.layout.questionnaire_item_drop_down_list, answerOptionString)
        autoCompleteTextView.setText(
          questionnaireItemViewItem.singleAnswerOrNull?.valueCoding?.display ?: ""
        )
        autoCompleteTextView.setAdapter(adapter)
        autoCompleteTextView.onItemClickListener =
          object : AdapterView.OnItemClickListener {
            override fun onItemClick(
              parent: AdapterView<*>?,
              view: View?,
              position: Int,
              id: Long
            ) {
              questionnaireItemViewItem.singleAnswerOrNull =
                QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent()
                  .setValue(
                    questionnaireItemViewItem.questionnaireItem.answerOption[position].valueCoding
                  )
            }
          }
      }
    }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.text.InputType
import org.hl7.fhir.r4.model.DecimalType
import org.hl7.fhir.r4.model.QuestionnaireResponse

internal object QuestionnaireItemEditTextDecimalViewHolderFactory :
  QuestionnaireItemEditTextViewHolderFactory() {
  override fun getQuestionnaireItemViewHolderDelegate() =
    object :
      QuestionnaireItemEditTextViewHolderDelegate(
        InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_SIGNED,
        isSingleLine = true
      ) {
      override fun getValue(
        text: String
      ): QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent? {
        return text.toDoubleOrNull()?.let {
          QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent()
            .setValue(DecimalType(it.toString()))
        }
      }

      override fun getText(
        answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent?
      ): String {
        return answer?.valueDecimalType?.value?.toString() ?: ""
      }
    }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.text.InputType
import org.hl7.fhir.r4.model.IntegerType
import org.hl7.fhir.r4.model.QuestionnaireResponse

internal object QuestionnaireItemEditTextIntegerViewHolderFactory :
  QuestionnaireItemEditTextViewHolderFactory() {
  override fun getQuestionnaireItemViewHolderDelegate() =
    object :
      QuestionnaireItemEditTextViewHolderDelegate(
        InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_SIGNED,
        true
      ) {
      override fun getValue(
        text: String
      ): QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent? {
        return text.toIntOrNull()?.let {
          QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(IntegerType(it))
        }
      }

      override fun getText(
        answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent?
      ): String {
        return answer?.valueIntegerType?.value?.toString() ?: ""
      }
    }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

internal object QuestionnaireItemEditTextMultiLineViewHolderFactory :
  QuestionnaireItemEditTextViewHolderFactory() {
  override fun getQuestionnaireItemViewHolderDelegate() =
    QuestionnaireItemEditTextStringViewHolderDelegate(false)
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.text.InputType
import org.hl7.fhir.r4.model.Quantity
import org.hl7.fhir.r4.model.QuestionnaireResponse

/**
 * Inherits from QuestionnaireItemEditTextViewHolderFactory as only the numeric part of the quantity
 * is being handled right now. Will use a separate layout to handle the unit in the quantity.
 */
internal object QuestionnaireItemEditTextQuantityViewHolderFactory :
  QuestionnaireItemEditTextViewHolderFactory() {
  override fun getQuestionnaireItemViewHolderDelegate() =
    object :
      QuestionnaireItemEditTextViewHolderDelegate(
        InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_SIGNED,
        isSingleLine = true
      ) {
      override fun getValue(
        text: String
      ): QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent? {
        return text.toDoubleOrNull()?.let {
          QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(Quantity(it))
        }
      }

      override fun getText(
        answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent?
      ): String {
        return answer?.valueQuantity?.value?.toString() ?: ""
      }
    }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

internal object QuestionnaireItemEditTextSingleLineViewHolderFactory :
  QuestionnaireItemEditTextViewHolderFactory() {
  override fun getQuestionnaireItemViewHolderDelegate() =
    QuestionnaireItemEditTextStringViewHolderDelegate(true)
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.text.InputType
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.StringType

/**
 * Implementation of [QuestionnaireItemEditTextViewHolderDelegate] used in
 * [QuestionnaireItemEditTextSingleLineViewHolderFactory] and
 * [QuestionnaireItemEditTextMultiLineViewHolderFactory].
 *
 * Any `ViewHolder` containing a `EditText` view that collects text data should use this class.
 */
internal class QuestionnaireItemEditTextStringViewHolderDelegate(isSingleLine: Boolean) :
  QuestionnaireItemEditTextViewHolderDelegate(InputType.TYPE_CLASS_TEXT, isSingleLine) {
  override fun getValue(
    text: String
  ): QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent? {
    return text.let {
      if (it.isEmpty()) {
        null
      } else {
        QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(StringType(it))
      }
    }
  }

  override fun getText(
    answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent?
  ): String {
    return answer?.valueStringType?.value ?: ""
  }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.text.Editable
import android.view.View
import android.widget.TextView
import androidx.core.widget.doAfterTextChanged
import com.google.android.fhir.datacapture.R
import com.google.android.material.textfield.TextInputEditText
import org.hl7.fhir.r4.model.QuestionnaireResponse

internal abstract class QuestionnaireItemEditTextViewHolderFactory :
  QuestionnaireItemViewHolderFactory(R.layout.questionnaire_item_edit_text_view) {
  abstract override fun getQuestionnaireItemViewHolderDelegate():
    QuestionnaireItemEditTextViewHolderDelegate
}

internal abstract class QuestionnaireItemEditTextViewHolderDelegate(
  private val rawInputType: Int,
  private val isSingleLine: Boolean
) : QuestionnaireItemViewHolderDelegate {
  private lateinit var prefixTextView: TextView
  private lateinit var textQuestion: TextView
  private lateinit var textInputEditText: TextInputEditText
  private lateinit var questionnaireItemViewItem: QuestionnaireItemViewItem

  override fun init(itemView: View) {
    prefixTextView = itemView.findViewById(R.id.prefix)
    textQuestion = itemView.findViewById(R.id.question)
    textInputEditText = itemView.findViewById(R.id.textInputEditText)
    textInputEditText.setRawInputType(rawInputType)
    textInputEditText.isSingleLine = isSingleLine
    textInputEditText.doAfterTextChanged { editable: Editable? ->
      questionnaireItemViewItem.singleAnswerOrNull = getValue(editable.toString())
      questionnaireItemViewItem.questionnaireResponseItemChangedCallback()
    }
  }

  override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) {
    this.questionnaireItemViewItem = questionnaireItemViewItem
    if (!questionnaireItemViewItem.questionnaireItem.prefix.isNullOrEmpty()) {
      prefixTextView.visibility = View.VISIBLE
      prefixTextView.text = questionnaireItemViewItem.questionnaireItem.prefix
    } else {
      prefixTextView.visibility = View.GONE
    }
    textQuestion.text = questionnaireItemViewItem.questionnaireItem.text
    textInputEditText.setText(getText(questionnaireItemViewItem.singleAnswerOrNull))
  }

  /** Returns the answer that should be recorded given the text input by the user. */
  abstract fun getValue(
    text: String
  ): QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent?

  /**
   * Returns the text that should be displayed in the [TextInputEditText] from the existing answer
   * to the question (may be input by the user or previously recorded).
   */
  abstract fun getText(
    answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent?
  ): String
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.view.View
import android.widget.TextView
import com.google.android.fhir.datacapture.R

internal object QuestionnaireItemGroupViewHolderFactory :
  QuestionnaireItemViewHolderFactory(R.layout.questionnaire_item_group_header_view) {
  override fun getQuestionnaireItemViewHolderDelegate() =
    object : QuestionnaireItemViewHolderDelegate {
      private lateinit var prefixTextView: TextView
      private lateinit var groupHeader: TextView

      override fun init(itemView: View) {
        prefixTextView = itemView.findViewById(R.id.prefix)
        groupHeader = itemView.findViewById(R.id.group_header)
      }

      override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) {
        if (!questionnaireItemViewItem.questionnaireItem.prefix.isNullOrEmpty()) {
          prefixTextView.visibility = View.VISIBLE
          prefixTextView.text = questionnaireItemViewItem.questionnaireItem.prefix
        } else {
          prefixTextView.visibility = View.GONE
        }
        groupHeader.text = questionnaireItemViewItem.questionnaireItem.text
        groupHeader.visibility =
          if (groupHeader.text.isEmpty()) {
            View.GONE
          } else {
            View.VISIBLE
          }
      }
    }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.view.View
import android.view.ViewGroup
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.TextView
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.displayString
import org.hl7.fhir.r4.model.QuestionnaireResponse

internal object QuestionnaireItemRadioGroupViewHolderFactory :
  QuestionnaireItemViewHolderFactory(R.layout.questionnaire_item_radio_group_view) {
  override fun getQuestionnaireItemViewHolderDelegate() =
    object : QuestionnaireItemViewHolderDelegate {
      private lateinit var prefixTextView: TextView
      private lateinit var radioHeader: TextView
      private lateinit var radioGroup: RadioGroup
      private lateinit var questionnaireItemViewItem: QuestionnaireItemViewItem

      override fun init(itemView: View) {
        prefixTextView = itemView.findViewById(R.id.prefix)
        radioGroup = itemView.findViewById(R.id.radio_group)
        radioHeader = itemView.findViewById(R.id.radio_header)
      }

      override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) {
        this.questionnaireItemViewItem = questionnaireItemViewItem
        if (!questionnaireItemViewItem.questionnaireItem.prefix.isNullOrEmpty()) {
          prefixTextView.visibility = View.VISIBLE
          prefixTextView.text = questionnaireItemViewItem.questionnaireItem.prefix
        } else {
          prefixTextView.visibility = View.GONE
        }
        val (questionnaireItem, questionnaireResponseItem) = questionnaireItemViewItem
        val answer = questionnaireResponseItem.answer.singleOrNull()?.valueCoding
        radioHeader.text = questionnaireItem.text
        radioGroup.removeAllViews()
        var index = 0
        questionnaireItem.answerOption.forEach {
          radioGroup.addView(
            RadioButton(radioGroup.context).apply {
              id = index++ // Use the answer option index as radio button ID
              text = it.displayString
              layoutParams =
                ViewGroup.LayoutParams(
                  ViewGroup.LayoutParams.MATCH_PARENT,
                  ViewGroup.LayoutParams.WRAP_CONTENT
                )
              isChecked = it.valueCoding.equalsDeep(answer)
            }
          )
        }
        radioGroup.setOnCheckedChangeListener { _, checkedId ->
          // if-else block to prevent over-writing of "items" nested within "answer"
          if (questionnaireResponseItem.answer.size > 0) {
            val tmpItems = questionnaireResponseItem.answer.first().item
            QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
              value = questionnaireItem.answerOption[checkedId].value
            }
          } else {
            questionnaireResponseItem.answer.apply {
              clear()
              add(
                QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
                  value = questionnaireItem.answerOption[checkedId].value
                }
              )
            }
          }
          questionnaireItemViewItem.questionnaireResponseItemChangedCallback()
        }
      }
    }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.view.View
import android.widget.TextView
import com.google.android.fhir.datacapture.R
import com.google.android.material.slider.Slider
import org.hl7.fhir.r4.model.IntegerType
import org.hl7.fhir.r4.model.QuestionnaireResponse

internal object QuestionnaireItemSliderViewHolderFactory :
  QuestionnaireItemViewHolderFactory(R.layout.questionnaire_item_slider) {
  override fun getQuestionnaireItemViewHolderDelegate(): QuestionnaireItemViewHolderDelegate =
    object : QuestionnaireItemViewHolderDelegate {
      private lateinit var prefixTextView: TextView
      private lateinit var sliderHeader: TextView
      private lateinit var slider: Slider
      private lateinit var questionnaireItemViewItem: QuestionnaireItemViewItem

      override fun init(itemView: View) {
        prefixTextView = itemView.findViewById(R.id.prefix)
        sliderHeader = itemView.findViewById(R.id.slider_header)
        slider = itemView.findViewById(R.id.slider)
      }

      override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) {
        this.questionnaireItemViewItem = questionnaireItemViewItem
        if (!questionnaireItemViewItem.questionnaireItem.prefix.isNullOrEmpty()) {
          prefixTextView.visibility = View.VISIBLE
          prefixTextView.text = questionnaireItemViewItem.questionnaireItem.prefix
        } else {
          prefixTextView.visibility = View.GONE
        }
        val questionnaireItem = questionnaireItemViewItem.questionnaireItem
        val answer = questionnaireItemViewItem.singleAnswerOrNull
        sliderHeader.text = questionnaireItem.text
        slider.valueFrom = 0.0F
        slider.valueTo = 100.0F
        slider.stepSize = 10.0F
        val sliderValue = answer?.valueIntegerType?.value?.toString() ?: "0.0"
        slider.value = sliderValue.toFloat()

        slider.addOnChangeListener { _, newValue, _ ->
          // Responds to when slider's value is changed
          questionnaireItemViewItem.singleAnswerOrNull =
            QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent()
              .setValue(IntegerType(newValue.toInt()))
          questionnaireItemViewItem.questionnaireResponseItemChangedCallback()
        }
      }
    }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.RecyclerView

/**
 * Factory for [QuestionnaireItemViewHolder].
 *
 * @param resId the layout resource for the view
 */
internal abstract class QuestionnaireItemViewHolderFactory(@LayoutRes val resId: Int) {
  internal fun create(parent: ViewGroup): QuestionnaireItemViewHolder {
    return QuestionnaireItemViewHolder(
      LayoutInflater.from(parent.context).inflate(resId, parent, false),
      getQuestionnaireItemViewHolderDelegate()
    )
  }

  /**
   * Returns a [QuestionnaireItemViewHolderDelegate] that handles the initialization of views and
   * binding of items in [RecyclerView].
   */
  abstract fun getQuestionnaireItemViewHolderDelegate(): QuestionnaireItemViewHolderDelegate
}

/**
 * The [RecyclerView.ViewHolder] for [QuestionnaireItemViewItem].
 *
 * This is used by [QuestionnaireItemAdapter] to initialize views and bind items in [RecyclerView].
 */
internal class QuestionnaireItemViewHolder(
  itemView: View,
  private val delegate: QuestionnaireItemViewHolderDelegate
) : RecyclerView.ViewHolder(itemView) {
  init {
    delegate.init(itemView)
  }

  fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) {
    delegate.bind(questionnaireItemViewItem)
  }
}

/**
 * Delegate for [QuestionnaireItemViewHolder].
 *
 * This interface provides an abstraction of the operations that need to be implemented for a type
 * of view in the questionnaire.
 *
 * There is a 1:1 relationship between this and [QuestionnaireItemViewHolder]. In other words, there
 * is a unique [QuestionnaireItemViewHolderDelegate] for each [QuestionnaireItemViewHolder]. This is
 * critical for the correctness of the recycler view.
 */
internal interface QuestionnaireItemViewHolderDelegate {
  /**
   * Initializes the view in [QuestionnaireItemViewHolder]. Any listeners to record user input
   * should be set in this function.
   */
  fun init(itemView: View)

  /** Binds a [QuestionnaireItemViewItem] to the view. */
  fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem)
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import androidx.recyclerview.widget.RecyclerView
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse

/**
 * Item for [QuestionnaireItemViewHolder] in [RecyclerView] containing
 * [Questionnaire.Item](the
 * question) and [QuestionnaireResponse.Item](the answer).
 *
 * [Questionnaire.Item](the question) and [QuestionnaireResponse.Item](the answer) are used to
 * create the right type of view (e.g. a CheckBox for a yes/no question) and populate the view with
 * the right information (e.g text for the CheckBox and initial yes/no answer for the CheckBox).
 *
 * @param questionnaireResponseItemChangedCallback function that should be called whenever the
 * `questionnaireResponseItemBuilder` is changed to inform the rest of the questionnaire to be
 * updated
 */
internal data class QuestionnaireItemViewItem(
  val questionnaireItem: Questionnaire.QuestionnaireItemComponent,
  val questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent,
  val questionnaireResponseItemChangedCallback: () -> Unit
) {
  /**
   * The single answer to the [QuestionnaireResponse.Item], or `null` if there is none or more than
   * one answer.
   */
  var singleAnswerOrNull
    get() = questionnaireResponseItem.answer.singleOrNull()
    set(value) {
      questionnaireResponseItem.answer.clear()
      value?.let { questionnaireResponseItem.addAnswer(it) }
    }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.views

import android.annotation.SuppressLint
import android.app.Dialog
import android.app.TimePickerDialog
import android.os.Bundle
import android.widget.TimePicker
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.setFragmentResult
import java.time.LocalTime

internal class TimePickerFragment : DialogFragment(), TimePickerDialog.OnTimeSetListener {
  @SuppressLint("NewApi") // Suppress warnings for java.time APIs
  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    // Use the current time as the default time in the picker
    val now = LocalTime.now()
    return TimePickerDialog(requireContext(), this, now.hour, now.minute, false)
  }

  override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) {
    setFragmentResult(
      RESULT_REQUEST_KEY,
      bundleOf(RESULT_BUNDLE_KEY_HOUR to hourOfDay, RESULT_BUNDLE_KEY_MINUTE to minute)
    )
    dismiss()
  }

  companion object {
    const val TAG = "time-picker-fragment"
    const val RESULT_REQUEST_KEY = "time-picker-request-key"
    const val RESULT_BUNDLE_KEY_HOUR = "time-picker-bundle-key-hour"
    const val RESULT_BUNDLE_KEY_MINUTE = "time-picker-bundle-key-minute"
  }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.gallery

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.google.android.fhir.datacapture.gallery.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
  private lateinit var binding: ActivityMainBinding
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
  }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.gallery

import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.navArgs
import ca.uhn.fhir.context.FhirContext
import com.google.android.fhir.datacapture.QuestionnaireFragment
import com.google.android.fhir.datacapture.gallery.databinding.FragmentQuestionnaireContainerBinding
import org.hl7.fhir.r4.model.QuestionnaireResponse

class QuestionnaireContainerFragment : Fragment() {
  private val viewModel: QuestionnaireViewModel by viewModels()
  private var _binding: FragmentQuestionnaireContainerBinding? = null
  private val binding
    get() = _binding!!
  private val args: QuestionnaireContainerFragmentArgs by navArgs()

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setHasOptionsMenu(true)
  }

  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View? {
    super.onCreate(savedInstanceState)
    _binding = FragmentQuestionnaireContainerBinding.inflate(inflater, container, false)
    arguments =
      bundleOf(
        QUESTIONNAIRE_FILE_PATH_KEY to args.questionnaireFilePathKey,
        QUESTIONNAIRE_RESPONSE_FILE_PATH_KEY to args.questionnaireResponseFilePathKey
      )
    (requireActivity() as AppCompatActivity).supportActionBar?.apply {
      title = args.questionnaireTitleKey
      setDisplayHomeAsUpEnabled(true)
    }
    // Only add the fragment once, when this fragment is first created.
    if (savedInstanceState == null) {
      val fragment = QuestionnaireFragment()
      fragment.arguments =
        bundleOf(QuestionnaireFragment.BUNDLE_KEY_QUESTIONNAIRE to viewModel.questionnaire)
      childFragmentManager.commit { add(R.id.container, fragment, QUESTIONNAIRE_FRAGMENT_TAG) }
    }
    return binding.root
  }

  override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
    inflater.inflate(R.menu.top_bar_menu, menu)
  }

  override fun onOptionsItemSelected(item: MenuItem): Boolean {
    return when (item.itemId) {
      R.id.action_submit -> {
        val questionnaireFragment =
          childFragmentManager.findFragmentByTag(QUESTIONNAIRE_FRAGMENT_TAG) as
            QuestionnaireFragment
        displayQuestionnaireResponse(questionnaireFragment.getQuestionnaireResponse())
        true
      }
      android.R.id.home -> {
        NavHostFragment.findNavController(this).navigateUp()
        true
      }
      else -> super.onOptionsItemSelected(item)
    }
  }

  // Display Questionnaire response as a dialog
  private fun displayQuestionnaireResponse(questionnaireResponse: QuestionnaireResponse) {
    val questionnaireResponseJson =
      FhirContext.forR4().newJsonParser().encodeResourceToString(questionnaireResponse)
    val dialogFragment = QuestionnaireResponseDialogFragment()
    dialogFragment.arguments =
      bundleOf(QuestionnaireResponseDialogFragment.BUNDLE_KEY_CONTENTS to questionnaireResponseJson)
    dialogFragment.show(childFragmentManager, QuestionnaireResponseDialogFragment.TAG)
  }

  companion object {
    const val QUESTIONNAIRE_FILE_PATH_KEY = "questionnaire-file-path-key"
    const val QUESTIONNAIRE_FRAGMENT_TAG = "questionnaire-fragment-tag"
    const val QUESTIONNAIRE_RESPONSE_FILE_PATH_KEY = "questionnaire-response-file-path-key"
  }

  override fun onDestroyView() {
    super.onDestroyView()
    (requireActivity() as AppCompatActivity).supportActionBar?.apply {
      setDisplayHomeAsUpEnabled(false)
      title = getString(R.string.app_name)
    }
    _binding = null
  }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.gallery

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.navigation.findNavController
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
import com.google.android.fhir.datacapture.gallery.databinding.QuestionnaireListItemViewBinding

class QuestionnaireListAdapter(private val questionnaireList: List<QuestionnaireListItem>) :
  Adapter<QuestionnaireListAdapter.ViewHolder>() {

  class ViewHolder(binding: QuestionnaireListItemViewBinding) :
    RecyclerView.ViewHolder(binding.root) {
    val name = binding.questionnaireName
    val description = binding.questionnaireDescription
    lateinit var questionnaireListItem: QuestionnaireListItem

    init {
      binding.root.setOnClickListener {
        val action =
          QuestionnaireListFragmentDirections
            .actionQuestionnaireListFragmentToQuestionnaireContainerFragment(
              questionnaireListItem.name,
              questionnaireListItem.questionnairePath,
              questionnaireListItem.questionnaireResponsePath
            )
        it.findNavController().navigate(action)
      }
    }
  }

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    return ViewHolder(
      QuestionnaireListItemViewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    )
  }

  override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val questionnaireListItem = questionnaireList[position]
    holder.questionnaireListItem = questionnaireListItem
    holder.name.text = questionnaireListItem.name
    holder.description.text = questionnaireListItem.description
  }

  override fun getItemCount() = questionnaireList.size
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.gallery

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.google.android.fhir.datacapture.gallery.databinding.FragmentQuestionnaireListBinding

class QuestionnaireListFragment : Fragment() {
  private var _binding: FragmentQuestionnaireListBinding? = null
  private val binding
    get() = _binding!!
  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View? {
    super.onCreate(savedInstanceState)
    _binding = FragmentQuestionnaireListBinding.inflate(inflater, container, false)

    binding.recyclerView.adapter =
      QuestionnaireListAdapter(
        listOf(
          // Example taken from
          // https://www.hl7.org/fhir/questionnaire-example-f201-lifelines.json.html
          QuestionnaireListItem(
            "Real-world lifelines questionnaire",
            "HL7 example \"f201\"",
            "hl7-questionnaire-example-f201-lifelines.json"
          ),
          // Example taken from
          // https://www.hl7.org/fhir/questionnaire-example-bluebook.json.html
          QuestionnaireListItem(
            "Neonate record from New South Wales, Australia",
            "HL7 example \"bb\"",
            "hl7-questionnaire-example-bluebook.json"
          ),
          // Example authored by fredhersch@google.com.
          QuestionnaireListItem(
            "Patient registration",
            "Example authored by Fred Hersch",
            "patient-registration.json"
          ),
          // Example taken from
          // https://openhie.github.io/hiv-ig/Questionnaire-hiv-case-report-questionnaire.json.html
          QuestionnaireListItem(
            "HIV Case Report",
            "HIV Case Reporting and Monitoring IG",
            "openhie-hiv-case-report.json"
          ),
          // Example taken from
          // https://openhie.github.io/covid-ig/Questionnaire-WhoCrQuestionnaireCovid19Surveillance.json.html
          QuestionnaireListItem(
            "COVID-19 Case Report",
            "WHO Case Reporting for COVID-19 Surveillance IG",
            "openhie-covid-case-report.json"
          ),
          // Example taken from
          // https://github.com/cqframework/cds4cpm-mypain/blob/master/src/content/mypain-questionnaire.json
          QuestionnaireListItem("MyPAIN", "CDS For Chronic Pain Management", "cds4cpm-mypain.json"),
          // IPRD-WITS Clinic HIV Risk assessment questionnaire authored by @joiskash
          QuestionnaireListItem(
            "HIV-Risk Assessment",
            "HIV-Risk Assessment",
            "iprd-hiv-fhir-questionnaire.json"
          )
        )
      )
    return binding.root
  }

  override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
  }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.gallery

/**
 * @param name the name of the questionnaire to be displayed
 * @param description the description of the questionnaire to be displayed
 * @param questionnairePath the questionnaire's JSON filename in the assets directory
 * @param questionnaireResponsePath the corresponding questionnaireResponse's JSON filename in the
 * assets directory (optional)
 */
data class QuestionnaireListItem(
  val name: String,
  val description: String,
  val questionnairePath: String,
  val questionnaireResponsePath: String?
) {

  constructor(
    name: String,
    description: String,
    questionnairePath: String
  ) : this(name, description, questionnairePath, null)
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.gallery

import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.google.android.fhir.datacapture.gallery.databinding.QuestionnaireResponseDialogContentsBinding

class QuestionnaireResponseDialogFragment() : DialogFragment() {
  private var _binding: QuestionnaireResponseDialogContentsBinding? = null
  private val binding
    get() = _binding!!
  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    val contents = requireArguments().getString(BUNDLE_KEY_CONTENTS)
    return activity?.let {
      _binding = QuestionnaireResponseDialogContentsBinding.inflate(layoutInflater)
      binding.contents.text = contents

      AlertDialog.Builder(it).setView(binding.root).create()
    }
      ?: throw IllegalStateException("Activity cannot be null")
  }

  companion object {
    const val TAG = "questionnaire-response-dialog-fragment"
    const val BUNDLE_KEY_CONTENTS = "contents"
  }

  override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
  }
}

/*
 * 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
 *
 *       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.android.fhir.datacapture.gallery

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle

class QuestionnaireViewModel(application: Application, private val state: SavedStateHandle) :
  AndroidViewModel(application) {
  private var questionnaireJson: String? = null
  val questionnaire: String
    get() {
      if (questionnaireJson == null) {
        questionnaireJson =
          readFileFromAssets(state[QuestionnaireContainerFragment.QUESTIONNAIRE_FILE_PATH_KEY]!!)
      }
      return questionnaireJson!!
    }
  private var questionnaireResponseJson: String? = null
  val questionnaireResponse: String?
    get() {
      if (questionnaireResponseJson == null) {
        if (state.contains(QuestionnaireContainerFragment.QUESTIONNAIRE_RESPONSE_FILE_PATH_KEY)) {
          questionnaireResponseJson =
            readFileFromAssets(
              state[QuestionnaireContainerFragment.QUESTIONNAIRE_RESPONSE_FILE_PATH_KEY]!!
            )
        }
      }
      return questionnaireResponseJson
    }

  private fun readFileFromAssets(filename: String): String {
    return getApplication<Application>().assets.open(filename).bufferedReader().use {
      it.readText()
    }
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference.api

import ca.uhn.fhir.parser.IParser
import java.lang.reflect.Type
import okhttp3.ResponseBody
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Resource
import retrofit2.Converter
import retrofit2.Retrofit

class FhirConverterFactory(private val parser: IParser) : Converter.Factory() {
  override fun responseBodyConverter(
    type: Type,
    annotations: Array<Annotation>,
    retrofit: Retrofit
  ): Converter<ResponseBody, *>? {
    return FhirConverter(parser)
  }
}

/** Retrofit converter that allows us to parse FHIR resources */
class FhirConverter(private val parser: IParser) : Converter<ResponseBody, Resource> {
  override fun convert(value: ResponseBody): Resource? {
    return parser.parseResource(Bundle::class.java, value.string()) as Resource
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference.api

import ca.uhn.fhir.parser.IParser
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.hl7.fhir.r4.model.Bundle
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Url

/** hapi.fhir.org API communication via Retrofit */
interface HapiFhirService {

  @GET suspend fun getResource(@Url url: String): Bundle

  companion object {
    const val BASE_URL = "https://hapi.fhir.org/baseR4/"

    fun create(parser: IParser): HapiFhirService {
      val logger = HttpLoggingInterceptor()
      logger.level = HttpLoggingInterceptor.Level.BODY

      val client = OkHttpClient.Builder().addInterceptor(logger).build()
      return Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(client)
        .addConverterFactory(FhirConverterFactory(parser))
        .addConverterFactory(GsonConverterFactory.create())
        .build()
        .create(HapiFhirService::class.java)
    }
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference.data

import android.content.Context
import androidx.work.WorkerParameters
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.reference.FhirApplication
import com.google.android.fhir.sync.PeriodicSyncWorker

class FhirPeriodicSyncWorker(appContext: Context, workerParams: WorkerParameters) :
  PeriodicSyncWorker(appContext, workerParams) {

  override fun getFhirEngine(): FhirEngine {
    return FhirApplication.fhirEngine(applicationContext)
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference.data

import com.google.android.fhir.reference.api.HapiFhirService
import com.google.android.fhir.sync.FhirDataSource
import org.hl7.fhir.r4.model.Bundle

/** Implementation of the [FhirDataSource] that communicates with hapi fhir. */
class HapiFhirResourceDataSource(private val service: HapiFhirService) : FhirDataSource {

  override suspend fun loadData(path: String): Bundle {
    return service.getResource(path)
  }
}

/*
 * 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
 *
 *       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.android.fhir.reference.data

import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.reference.PatientListViewModel
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Observation
import org.hl7.fhir.r4.model.Patient

private const val MAX_RESOURCE_COUNT = 20

/**
 * Helper class for loading a list of sample Fhir Patient Resource objects.
 *
 * Parses and loads Patient data from the passed JSON String into items that could be used by
 * PatientListViewModel.
 */
class SamplePatients {
  // The resource bundle with Patient objects.
  private var fhirBundle: Bundle? = null

  companion object {
    val tag = "SamplePatient"

    val fhirJsonParser: IParser = FhirContext.forR4().newJsonParser()
  }
  /** Returns list of PatientItem objects based on patients from the json string. */
  fun getPatientItems(jsonString: String): List<PatientListViewModel.PatientItem> {
    val patients: MutableList<PatientListViewModel.PatientItem> = mutableListOf()

    fhirBundle = fhirJsonParser.parseResource(Bundle::class.java, jsonString) as Bundle

    // Create a list of PatientItems from fhirPatients. The display index is 1 based.
    fhirBundle
      ?.entry
      ?.take(MAX_RESOURCE_COUNT)
      ?.mapIndexed { index, entry -> createPatientItem(index + 1, entry.resource as Patient) }
      ?.let { patients.addAll(it) }

    return patients
  }

  fun getPatientItems(fhirPatients: List<Patient>): List<PatientListViewModel.PatientItem> {
    val patients: MutableList<PatientListViewModel.PatientItem> = mutableListOf()

    // Create a list of PatientItems from fhirPatients. The display index is 1 based.
    fhirPatients
      .take(MAX_RESOURCE_COUNT)
      ?.mapIndexed { index, fhirPatient -> createPatientItem(index + 1, fhirPatient) }
      ?.let { patients.addAll(it) }

    // Return a cloned List
    return patients
  }

  /** Creates PatientItem objects with displayable values from the Fhir Patient objects. */
  private fun createPatientItem(position: Int, patient: Patient): PatientListViewModel.PatientItem {
    val name = patient.name[0].nameAsSingleString

    // Show nothing if no values available for gender and date of birth.
    val gender = if (patient.hasGenderElement()) patient.genderElement.valueAsString else ""
    val dob = if (patient.hasBirthDateElement()) patient.birthDateElement.valueAsString else ""
    val html: String = if (patient.hasText()) patient.text.div.valueAsString else ""
    val phone: String = if (patient.hasTelecom()) patient.telecom[0].value else ""

    return PatientListViewModel.PatientItem(position.toString(), name, gender, dob, html, phone)
  }

  /** Returns list of ObservationItem objects based on observations from the json string. */
  fun getObservationItems(jsonString: String): MutableList<PatientListViewModel.ObservationItem> {
    val observations = ArrayList<PatientListViewModel.ObservationItem>()

    fhirBundle = fhirJsonParser.parseResource(Bundle::class.java, jsonString) as Bundle

    // Create a list of ObservationItems from fhirObservations. The display index is 1 based.
    fhirBundle
      ?.entry
      ?.take(MAX_RESOURCE_COUNT)
      ?.mapIndexed { index, entry ->
        createObservationItem(index + 1, entry.resource as Observation)
      }
      ?.let { observations.addAll(it) }

    return observations
  }

  /** Creates ObservationItem objects with displayable values from the Fhir Observation objects. */
  private fun createObservationItem(
    position: Int,
    observation: Observation
  ): PatientListViewModel.ObservationItem {
    // val observation: Observation = getObservationDetails(position)
    val observationCode = observation.code.text

    // Show nothing if no values available for datetime and value quantity.
    val dateTimeStr =
      if (observation.hasEffectiveDateTimeType()) observation.effectiveDateTimeType.asStringValue()
      else "No effective DateTime"
    val value =
      if (observation.hasValueQuantity()) observation.valueQuantity.value.toString()
      else "No ValueQuantity"
    val valueUnit = if (observation.hasValueQuantity()) observation.valueQuantity.unit else ""
    val valueStr = "$value $valueUnit"

    return PatientListViewModel.ObservationItem(
      position.toString(),
      observationCode,
      dateTimeStr,
      valueStr
    )
  }
}

/*
 * 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
 *
 *       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.android.fhir.db.impl.dao

import android.util.Log
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.db.impl.entities.LocalChangeEntity
import com.google.android.fhir.db.impl.entities.LocalChangeEntity.Type
import com.google.android.fhir.toTimeZoneString
import java.util.Date
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType

/**
 * Dao for local changes made to a resource. One row in LocalChangeEntity corresponds to one change
 * e.g. an INSERT or UPDATE. The UPDATES (diffs) are stored as RFC 6902 JSON patches. When a
 * resource needs to be synced, all corresponding LocalChanges are 'squashed' to create a a single
 * LocalChangeEntity to sync with the server.
 */
@Dao
internal abstract class LocalChangeDao {

  lateinit var iParser: IParser

  @Insert abstract fun addLocalChange(localChangeEntity: LocalChangeEntity)

  @Transaction
  open fun addInsertAll(resources: List<Resource>) {
    resources.forEach { resource -> addInsert(resource) }
  }

  fun addInsert(resource: Resource) {
    val resourceId = resource.id
    val resourceType = resource.resourceType
    val timestamp = Date().toTimeZoneString()
    val resourceString = iParser.encodeResourceToString(resource)

    addLocalChange(
      LocalChangeEntity(
        id = 0,
        resourceType = resourceType.name,
        resourceId = resourceId,
        timestamp = timestamp,
        type = Type.INSERT,
        payload = resourceString
      )
    )
  }

  fun addUpdate(oldResource: Resource, resource: Resource) {
    val resourceId = resource.id
    val resourceType = resource.resourceType
    val timestamp = Date().toTimeZoneString()

    if (!localChangeIsEmpty(resourceId, resourceType) &&
        lastChangeType(resourceId, resourceType)!!.equals(Type.DELETE)
    ) {
      throw InvalidLocalChangeException(
        "Unexpected DELETE when updating $resourceType/$resourceId. UPDATE failed."
      )
    }
    val jsonDiff = LocalChangeUtils.diff(iParser, oldResource, resource)
    if (jsonDiff.length() == 0) {
      Log.i(
        "LocalChangeDao",
        "New resource ${resource.resourceType}/${resource.id} is same as old resource. " +
          "Not inserting UPDATE LocalChange."
      )
      return
    }
    addLocalChange(
      LocalChangeEntity(
        id = 0,
        resourceType = resourceType.name,
        resourceId = resourceId,
        timestamp = timestamp,
        type = Type.UPDATE,
        payload = jsonDiff.toString()
      )
    )
  }

  fun addDelete(resourceId: String, resourceType: ResourceType) {
    val timestamp = Date().toTimeZoneString()
    addLocalChange(
      LocalChangeEntity(
        id = 0,
        resourceType = resourceType.name,
        resourceId = resourceId,
        timestamp = timestamp,
        type = Type.DELETE,
        payload = ""
      )
    )
  }

  @Query(
    """
        SELECT type
        FROM LocalChangeEntity
        WHERE resourceId = :resourceId
        AND resourceType = :resourceType
        ORDER BY id ASC
        LIMIT 1
    """
  )
  abstract fun lastChangeType(resourceId: String, resourceType: ResourceType): Type?

  @Query(
    """
        SELECT COUNT(type)
        FROM LocalChangeEntity
        WHERE resourceId = :resourceId
        AND resourceType = :resourceType
        LIMIT 1
    """
  )
  abstract fun countLastChange(resourceId: String, resourceType: ResourceType): Int

  private fun localChangeIsEmpty(resourceId: String, resourceType: ResourceType): Boolean =
    countLastChange(resourceId, resourceType) == 0

  @Query(
    """
        SELECT *
        FROM LocalChangeEntity
        ORDER BY LocalChangeEntity.id ASC"""
  )
  abstract fun getAllLocalChanges(): List<LocalChangeEntity>

  @Query(
    """
        DELETE FROM LocalChangeEntity
        WHERE LocalChangeEntity.id = (:id)
    """
  )
  abstract fun discardLocalChanges(id: Long)

  fun discardLocalChanges(token: LocalChangeToken) {
    token.ids.forEach { discardLocalChanges(it) }
  }

  class InvalidLocalChangeException(message: String?) : Exception(message)
}

/*
 * 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
 *
 *       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.android.fhir.db.impl.dao

data class LocalChangeToken(val ids: List<Long>)

/*
 * 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
 *
 *       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.android.fhir.db.impl.dao

import ca.uhn.fhir.parser.IParser
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.fge.jsonpatch.JsonPatch
import com.github.fge.jsonpatch.diff.JsonDiff
import com.google.android.fhir.db.impl.entities.LocalChangeEntity
import java.lang.IllegalArgumentException
import org.hl7.fhir.r4.model.Resource
import org.json.JSONArray
import org.json.JSONObject

object LocalChangeUtils {

  /** Squash the changes by merging them two at a time. */
  fun squash(localChangeEntities: List<LocalChangeEntity>): LocalChangeEntity =
    localChangeEntities.reduce { first, second -> mergeLocalChanges(first, second) }

  fun mergeLocalChanges(first: LocalChangeEntity, second: LocalChangeEntity): LocalChangeEntity {
    val type: LocalChangeEntity.Type
    val payload: String
    when (second.type) {
      LocalChangeEntity.Type.UPDATE ->
        when {
          first.type.equals(LocalChangeEntity.Type.UPDATE) -> {
            type = LocalChangeEntity.Type.UPDATE
            payload = mergePatches(first.payload, second.payload)
          }
          first.type.equals(LocalChangeEntity.Type.INSERT) -> {
            type = LocalChangeEntity.Type.INSERT
            payload = applyPatch(first.payload, second.payload)
          }
          else -> {
            throw IllegalArgumentException(
              "Cannot merge local changes with type ${first.type} and ${second.type}."
            )
          }
        }
      LocalChangeEntity.Type.DELETE -> {
        type = LocalChangeEntity.Type.DELETE
        payload = ""
      }
      LocalChangeEntity.Type.INSERT -> {
        type = LocalChangeEntity.Type.INSERT
        payload = second.payload
      }
    }
    return LocalChangeEntity(
      id = 0,
      resourceId = second.resourceId,
      resourceType = second.resourceType,
      type = type,
      payload = payload
    )
  }

  /** Update a JSON object with a JSON patch (RFC 6902). */
  private fun applyPatch(resourceString: String, patchString: String): String {
    val objectMapper = ObjectMapper()
    val resourceJson = objectMapper.readValue(resourceString, JsonNode::class.java)
    val patchJson = objectMapper.readValue(patchString, JsonPatch::class.java)
    return patchJson.apply(resourceJson).toString()
  }

  /** Merge two JSON patch strings by concatenating their elements into a new JSON array. */
  private fun mergePatches(firstPatch: String, secondPatch: String): String {
    // TODO: validate patches are RFC 6902 compliant JSON patches
    val firstMap = JSONArray(firstPatch).patchMergeMap()
    val secondMap = JSONArray(secondPatch).patchMergeMap()
    firstMap.putAll(secondMap)
    return JSONArray(firstMap.values).toString()
  }

  /** Calculates the JSON patch between two [Resource] s. */
  internal fun diff(parser: IParser, source: Resource, target: Resource): JSONArray {
    val objectMapper = ObjectMapper()
    val jsonDiff =
      JsonDiff.asJson(
        objectMapper.readValue(parser.encodeResourceToString(source), JsonNode::class.java),
        objectMapper.readValue(parser.encodeResourceToString(target), JsonNode::class.java)
      )
    return JSONArray(jsonDiff.toString())
  }

  /**
   * Creates a mutable map from operation type (e.g. add/remove) + property path to the entire
   * operation containing the updated value. Two such maps can be merged using `Map.putAll()` to
   * yield a minimal set of operations equivalent to individual patches.
   */
  private fun JSONArray.patchMergeMap(): MutableMap<Pair<String, String>, JSONObject> {
    return (0 until this.length())
      .map { this.optJSONObject(it) }
      .associateBy { it.optString("op") to it.optString("path") }
      .toMutableMap()
  }
}

/*
 * 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
 *
 *       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.android.fhir.db.impl.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.sqlite.db.SupportSQLiteQuery
import ca.uhn.fhir.parser.IParser
import ca.uhn.fhir.rest.annotation.Transaction
import com.google.android.fhir.db.impl.entities.DateIndexEntity
import com.google.android.fhir.db.impl.entities.NumberIndexEntity
import com.google.android.fhir.db.impl.entities.QuantityIndexEntity
import com.google.android.fhir.db.impl.entities.ReferenceIndexEntity
import com.google.android.fhir.db.impl.entities.ResourceEntity
import com.google.android.fhir.db.impl.entities.StringIndexEntity
import com.google.android.fhir.db.impl.entities.TokenIndexEntity
import com.google.android.fhir.db.impl.entities.UriIndexEntity
import com.google.android.fhir.index.ResourceIndexer
import com.google.android.fhir.index.ResourceIndices
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType

@Dao
internal abstract class ResourceDao {
  // this is ugly but there is no way to inject these right now in Room as it is the one creating
  // the dao
  lateinit var iParser: IParser

  @Transaction
  open fun update(resource: Resource) {
    updateResource(resource.id, resource.resourceType, iParser.encodeResourceToString(resource))
    val entity =
      ResourceEntity(
        id = 0,
        resourceType = resource.resourceType,
        resourceId = resource.id,
        serializedResource = iParser.encodeResourceToString(resource)
      )
    val index = ResourceIndexer.index(resource)
    updateIndicesForResource(index, entity)
  }

  @Transaction
  open fun insert(resource: Resource) {
    insertResource(resource)
  }

  @Transaction
  open fun insertAll(resources: List<Resource>) {
    resources.forEach { resource -> insertResource(resource) }
  }

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  abstract fun insertResource(resource: ResourceEntity)

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  abstract fun insertStringIndex(stringIndexEntity: StringIndexEntity)

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  abstract fun insertReferenceIndex(referenceIndexEntity: ReferenceIndexEntity)

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  abstract fun insertCodeIndex(tokenIndexEntity: TokenIndexEntity)

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  abstract fun insertQuantityIndex(quantityIndexEntity: QuantityIndexEntity)

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  abstract fun insertUriIndex(uriIndexEntity: UriIndexEntity)

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  abstract fun insertDateIndex(dateIndexEntity: DateIndexEntity)

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  abstract fun insertNumberIndex(numberIndexEntity: NumberIndexEntity)

  @Query(
    """
        UPDATE ResourceEntity
        SET serializedResource = :serializedResource
        WHERE resourceId = :resourceId
        AND resourceType = :resourceType
        """
  )
  abstract fun updateResource(
    resourceId: String,
    resourceType: ResourceType,
    serializedResource: String
  )

  @Query(
    """
        DELETE FROM ResourceEntity
        WHERE resourceId = :resourceId AND resourceType = :resourceType"""
  )
  abstract fun deleteResource(resourceId: String, resourceType: ResourceType): Int

  @Query(
    """
        SELECT serializedResource
        FROM ResourceEntity
        WHERE resourceId = :resourceId AND resourceType = :resourceType"""
  )
  abstract fun getResource(resourceId: String, resourceType: ResourceType): String?

  @Query(
    """
        SELECT ResourceEntity.serializedResource
        FROM ResourceEntity
        JOIN ReferenceIndexEntity
        ON ResourceEntity.resourceType = ReferenceIndexEntity.resourceType
            AND ResourceEntity.resourceId = ReferenceIndexEntity.resourceId
        WHERE ReferenceIndexEntity.resourceType = :resourceType
            AND ReferenceIndexEntity.index_path = :indexPath
            AND ReferenceIndexEntity.index_value = :indexValue"""
  )
  abstract fun getResourceByReferenceIndex(
    resourceType: String,
    indexPath: String,
    indexValue: String
  ): List<String>

  @Query(
    """
        SELECT ResourceEntity.serializedResource
        FROM ResourceEntity
        JOIN StringIndexEntity
        ON ResourceEntity.resourceType = StringIndexEntity.resourceType
            AND ResourceEntity.resourceId = StringIndexEntity.resourceId
        WHERE StringIndexEntity.resourceType = :resourceType
            AND StringIndexEntity.index_path = :indexPath
            AND StringIndexEntity.index_value = :indexValue"""
  )
  abstract fun getResourceByStringIndex(
    resourceType: String,
    indexPath: String,
    indexValue: String
  ): List<String>

  @Query(
    """
        SELECT ResourceEntity.serializedResource
        FROM ResourceEntity
        JOIN TokenIndexEntity
        ON ResourceEntity.resourceType = TokenIndexEntity.resourceType
            AND ResourceEntity.resourceId = TokenIndexEntity.resourceId
        WHERE TokenIndexEntity.resourceType = :resourceType
            AND TokenIndexEntity.index_path = :indexPath
            AND TokenIndexEntity.index_system = :indexSystem
            AND TokenIndexEntity.index_value = :indexValue"""
  )
  abstract fun getResourceByCodeIndex(
    resourceType: String,
    indexPath: String,
    indexSystem: String,
    indexValue: String
  ): List<String>

  @RawQuery abstract fun getResources(query: SupportSQLiteQuery): List<String>

  private fun insertResource(resource: Resource) {
    val entity =
      ResourceEntity(
        id = 0,
        resourceType = resource.resourceType,
        resourceId = resource.id,
        serializedResource = iParser.encodeResourceToString(resource)
      )
    insertResource(entity)
    val index = ResourceIndexer.index(resource)
    updateIndicesForResource(index, entity)
  }

  private fun updateIndicesForResource(index: ResourceIndices, resource: ResourceEntity) {
    // TODO Move StringIndices to persistable types
    //  https://github.com/jingtang10/fhir-engine/issues/31
    //  we can either use room-autovalue integration or go w/ embedded data classes.
    //  we may also want to merge them:
    //  https://github.com/jingtang10/fhir-engine/issues/33
    index.stringIndices.forEach {
      insertStringIndex(
        StringIndexEntity(
          id = 0,
          resourceType = resource.resourceType,
          index = it,
          resourceId = resource.resourceId
        )
      )
    }
    index.referenceIndices.forEach {
      insertReferenceIndex(
        ReferenceIndexEntity(
          id = 0,
          resourceType = resource.resourceType,
          index = it,
          resourceId = resource.resourceId
        )
      )
    }
    index.tokenIndices.forEach {
      insertCodeIndex(
        TokenIndexEntity(
          id = 0,
          resourceType = resource.resourceType,
          index = it,
          resourceId = resource.resourceId
        )
      )
    }
    index.quantityIndices.forEach {
      insertQuantityIndex(
        QuantityIndexEntity(
          id = 0,
          resourceType = resource.resourceType,
          index = it,
          resourceId = resource.resourceId
        )
      )
    }
    index.uriIndices.forEach {
      insertUriIndex(
        UriIndexEntity(
          id = 0,
          resourceType = resource.resourceType,
          index = it,
          resourceId = resource.resourceId
        )
      )
    }
    index.dateIndices.forEach {
      insertDateIndex(
        DateIndexEntity(
          id = 0,
          resourceType = resource.resourceType,
          index = it,
          resourceId = resource.resourceId
        )
      )
    }
    index.numberIndices.forEach {
      insertNumberIndex(
        NumberIndexEntity(
          id = 0,
          resourceType = resource.resourceType,
          index = it,
          resourceId = resource.resourceId
        )
      )
    }
  }
}

/*
 * 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
 *
 *       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.android.fhir.db.impl.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.google.android.fhir.db.impl.entities.SyncedResourceEntity
import org.hl7.fhir.r4.model.ResourceType

@Dao
interface SyncedResourceDao {

  @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(entity: SyncedResourceEntity)

  /**
   * We will always have 1 entry for each [ResourceType] as it's the primary key, so we can limit
   * the result to 1. If there is no entry for that [ResourceType] then `null` will be returned.
   */
  @Query(
    """SELECT lastUpdate FROM SyncedResourceEntity
        WHERE resourceType = :resourceType LIMIT 1"""
  )
  suspend fun getLastUpdate(resourceType: ResourceType): String?
}

/*
 * 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
 *
 *       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.android.fhir.db.impl.entities

import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.android.fhir.index.entities.DateIndex
import org.hl7.fhir.r4.model.ResourceType

@Entity(
  indices =
    [
      Index(value = ["resourceType", "index_name", "index_tsHigh", "index_tsLow"]),
      Index(
        // keep this index for faster foreign lookup
        value = ["resourceId", "resourceType"]
      )],
  foreignKeys =
    [
      ForeignKey(
        entity = ResourceEntity::class,
        parentColumns = ["resourceId", "resourceType"],
        childColumns = ["resourceId", "resourceType"],
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.NO_ACTION,
        deferred = true
      )]
)
internal data class DateIndexEntity(
  @PrimaryKey(autoGenerate = true) val id: Long,
  val resourceType: ResourceType,
  @Embedded(prefix = "index_") val index: DateIndex,
  val resourceId: String
)

/*
 * 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
 *
 *       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.android.fhir.db.impl.entities

import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey

/**
 * When a local change to a resource happens, the lastUpdated timestamp in [ResourceEntity] is
 * updated and the diff itself is inserted in this table. The value of the diff depends upon the
 * type of change and can be:
 * * DELETE: The empty string, "".
 * * INSERT: The full resource in JSON form, e.g. {
 * ```
 *      "resourceType": "Patient",
 *      "id": "animal",
 *      "name": [
 *              {
 *               "use": "usual",
 *               "given": [
 *               "Kenzi"
 *              ]
 *              }
 *        ],
 *      ...
 * ```
 * }
 * * UPDATE: A RFC 6902 JSON patch. e.g. a patch that changes the given name of a patient: [
 * ```
 *      {
 *      "op": "replace",
 *      "path": "/name/0/given/0",
 *      "value": "Binny"
 *      }
 * ```
 * ] For resource that is fully synced with server this table should not have any rows.
 */
@Entity(indices = [Index(value = ["resourceType", "resourceId"])])
data class LocalChangeEntity(
  @PrimaryKey(autoGenerate = true) val id: Long,
  val resourceType: String,
  val resourceId: String,
  val timestamp: String = "",
  val type: Type,
  val payload: String
) {
  enum class Type(val value: Int) {
    INSERT(1), // create a new resource. payload is the entire resource json.
    UPDATE(2), // patch. payload is the json patch.
    DELETE(3); // delete. payload is empty string.

    companion object {
      fun from(input: Int): Type = values().first { it.value == input }
    }
  }
}

/*
 * 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
 *
 *       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.android.fhir.db.impl.entities

import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.android.fhir.index.entities.NumberIndex
import org.hl7.fhir.r4.model.ResourceType

@Entity(
  indices =
    [
      Index(value = ["resourceType", "index_name", "index_value"]),
      Index(
        // keep this index for faster foreign lookup
        value = ["resourceId", "resourceType"]
      )],
  foreignKeys =
    [
      ForeignKey(
        entity = ResourceEntity::class,
        parentColumns = ["resourceId", "resourceType"],
        childColumns = ["resourceId", "resourceType"],
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.NO_ACTION,
        deferred = true
      )]
)
internal data class NumberIndexEntity(
  @PrimaryKey(autoGenerate = true) val id: Long,
  val resourceType: ResourceType,
  @Embedded(prefix = "index_") val index: NumberIndex,
  val resourceId: String
)

/*
 * 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
 *
 *       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.android.fhir.db.impl.entities

import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.android.fhir.index.entities.QuantityIndex
import org.hl7.fhir.r4.model.ResourceType

@Entity(
  indices =
    [
      Index(value = ["resourceType", "index_name", "index_value"]),
      Index(
        // keep this index for faster foreign lookup
        value = ["resourceId", "resourceType"]
      )],
  foreignKeys =
    [
      ForeignKey(
        entity = ResourceEntity::class,
        parentColumns = ["resourceId", "resourceType"],
        childColumns = ["resourceId", "resourceType"],
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.NO_ACTION,
        deferred = true
      )]
)
internal data class QuantityIndexEntity(
  @PrimaryKey(autoGenerate = true) val id: Long,
  val resourceType: ResourceType,
  val resourceId: String,
  @Embedded(prefix = "index_") val index: QuantityIndex
)

/*
 * 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
 *
 *       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.android.fhir.db.impl.entities

import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.android.fhir.index.entities.ReferenceIndex
import org.hl7.fhir.r4.model.ResourceType

@Entity(
  indices =
    [
      Index(value = ["resourceType", "index_name", "index_value"]),
      Index(
        // keep this index for faster foreign lookup
        value = ["resourceId", "resourceType"]
      )],
  foreignKeys =
    [
      ForeignKey(
        entity = ResourceEntity::class,
        parentColumns = ["resourceId", "resourceType"],
        childColumns = ["resourceId", "resourceType"],
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.NO_ACTION,
        deferred = true
      )]
)
internal data class ReferenceIndexEntity(
  @PrimaryKey(autoGenerate = true) val id: Long,
  val resourceType: ResourceType,
  @Embedded(prefix = "index_") val index: ReferenceIndex,
  val resourceId: String
)

/*
 * 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
 *
 *       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.android.fhir.db.impl.entities

import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import org.hl7.fhir.r4.model.ResourceType

@Entity(indices = [Index(value = ["resourceType", "resourceId"], unique = true)])
internal data class ResourceEntity(
  @PrimaryKey(autoGenerate = true) val id: Long,
  val resourceType: ResourceType,
  val resourceId: String,
  val serializedResource: String
)

/*
 * 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
 *
 *       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.android.fhir.db.impl.entities

import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.android.fhir.index.entities.StringIndex
import org.hl7.fhir.r4.model.ResourceType

@Entity(
  indices =
    [
      Index(value = ["resourceType", "index_name", "index_value"]),
      Index(
        // keep this index for faster foreign lookup
        value = ["resourceId", "resourceType"]
      )],
  foreignKeys =
    [
      ForeignKey(
        entity = ResourceEntity::class,
        parentColumns = ["resourceId", "resourceType"],
        childColumns = ["resourceId", "resourceType"],
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.NO_ACTION,
        deferred = true
      )]
)
internal data class StringIndexEntity(
  @PrimaryKey(autoGenerate = true) val id: Long,
  val resourceType: ResourceType,
  @Embedded(prefix = "index_") val index: StringIndex,
  val resourceId: String
)

/*
 * 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
 *
 *       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.android.fhir.db.impl.entities

import androidx.room.Entity
import androidx.room.PrimaryKey
import org.hl7.fhir.r4.model.ResourceType

/**
 * Class that models a table that holds all resource types that were synced and the highest
 * `_lastUpdate` value of each resource type.
 */
@Entity
data class SyncedResourceEntity(
  /** Resource synced */
  @PrimaryKey val resourceType: ResourceType,
  /** The highest `_lastUpdate` value of the resources synced of a specific type */
  val lastUpdate: String
)

/*
 * 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
 *
 *       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.android.fhir.db.impl.entities

import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.android.fhir.index.entities.TokenIndex
import org.hl7.fhir.r4.model.ResourceType

@Entity(
  indices =
    [
      Index(value = ["resourceType", "index_name", "index_system", "index_value"]),
      Index(
        // keep this index for faster foreign lookup
        value = ["resourceId", "resourceType"]
      )],
  foreignKeys =
    [
      ForeignKey(
        entity = ResourceEntity::class,
        parentColumns = ["resourceId", "resourceType"],
        childColumns = ["resourceId", "resourceType"],
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.NO_ACTION,
        deferred = true
      )]
)
internal data class TokenIndexEntity(
  @PrimaryKey(autoGenerate = true) val id: Long,
  val resourceType: ResourceType,
  @Embedded(prefix = "index_") val index: TokenIndex,
  val resourceId: String
)

/*
 * 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
 *
 *       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.android.fhir.db.impl.entities

import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.android.fhir.index.entities.UriIndex
import org.hl7.fhir.r4.model.ResourceType

@Entity(
  indices =
    [
      Index(value = ["resourceType", "index_name", "index_uri"]),
      Index(
        // keep this index for faster foreign lookup
        value = ["resourceId", "resourceType"]
      )],
  foreignKeys =
    [
      ForeignKey(
        entity = ResourceEntity::class,
        parentColumns = ["resourceId", "resourceType"],
        childColumns = ["resourceId", "resourceType"],
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.NO_ACTION,
        deferred = true
      )]
)
internal data class UriIndexEntity(
  @PrimaryKey(autoGenerate = true) val id: Long,
  val resourceType: ResourceType,
  val resourceId: String,
  @Embedded(prefix = "index_") val index: UriIndex
)