import { applySnapshot, flow, getParent, getSnapshot, getType, Instance, types } from 'mobx-state-tree'
import type { IAnyModelType, IModelType } from 'mobx-state-tree/dist/types/complex-types/model'
import type { MSTGQLStore, Query } from 'mst-gql'
import { getPath } from './utils'
import { createNativeStackNavigator, NativeStackScreenProps } from '@react-navigation/native-stack'
import React, { Component, FC, ReactElement } from 'react'
import { observer } from 'mobx-react-lite'
import type { IAnyType } from 'mobx-state-tree/dist/internal'
import type { IOptionalIType } from 'mobx-state-tree/dist/types/utility-types/optional'
import type { StyledProps } from 'native-base/src/theme/types'
import type { GestureResponderEvent } from 'react-native'
import EntityListScreen from './EntityListScreen'
import type BaseSchema from 'yup/lib/schema'
import type { ObjectSchema } from 'yup'
import EntityM2MAssocScreen from './EntityM2MAssocScreen'
import EntityDetailsScreen from './EntityDetailsScreen'
import EntityCreateEditScreen from './EntityCreateEditScreen'
import * as Yup from 'yup'
import { EntityData } from './EntityData'

export enum QueryState {
  PENDING = 'PENDING',
  DONE = 'DONE',
  ERROR = 'ERROR',
}

export enum AssocSelectionType {
  NONE = 'NONE',
  SINGLE = 'SINGLE',
  MULTIPLE = 'MULTIPLE',
}

const defaultOrderBy = [{ id: 'desc' }]

// export interface ModelBaseType extends Instance<typeof ModelBase.Type> {}

/**
 * Creates an MST model for listing, filtering, editing and creating records of type `ModelType`.
 *
 * As we basically want to provide the same kind of DataTable based visualisation for handling all our records it would be tedious to
 * generate a model, which basically always does the same but for different mst-gql generated model types. So, instead we have
 * createEntityModel to generate and MST model for any entity type we have providing as a configuration selection sets
 * for listing and loading an entity (`listOperationSelectionSet`, `loadOperationSelectionSet`) and functions for updating and creating these
 * entities (`updateMutation`, `createMutation`).
 *
 * Accompanying a model created by createEntityModel() there will be generic screens as well, which handle the common
 * functionality of listing these entities; creation and editing requires somewhat specific forms for each entity type, so these will
 * not be generalized.
 *
 * @param modelConfig
 */
export function createEntityModel<
  ModelType,
  BoolExpType,
  OrderByType,
  UpdateParamType,
  CreateParamType,
  GqlStoreType extends StoreType
>(
  modelConfig: EntityConfig<
    ModelType | unknown,
    UpdateParamType | unknown,
    CreateParamType | unknown,
    BoolExpType | unknown
  >
) {
  let model = types
    .model(modelConfig.modelName, {
      // Current page we are displaying (limit for Hasura query)
      currentPage: types.optional(types.number, 0),

      // Number of items to be displayed on a single page (offset for Hasura query)
      pageSize: types.optional(types.number, 10),

      // Number of all items we may display
      count: types.maybeNull(types.number),

      // Fields that mut be specified when creating/updating the objects to relate to another entity. Eg. Location.serviceAccount
      joinFields: types.maybeNull(types.frozen<{ [key: string]: any }>()),

      // Basic where condition to which filtering self.where is applied
      baseWhere: types.maybeNull(types.frozen<BoolExpType>()),

      // Query options
      where: types.maybeNull(types.frozen<BoolExpType>()),
      orderBy: types.optional(types.frozen<Array<OrderByType>>(), defaultOrderBy as unknown as OrderByType[]),

      // Current state of the listing process
      listQueryState: types.maybeNull(types.enumeration<QueryState>('QueryState', Object.values(QueryState))),

      // Error message in case listQueryState == error
      listQueryError: types.maybe(types.string),

      // Current state of the edited item mutation process
      updateMutationState: types.maybeNull(types.enumeration<QueryState>('QueryState', Object.values(QueryState))),

      // Error message in case updateMutationState == error
      updateMutationError: types.maybeNull(types.string),

      // Current state of the created item mutation process
      createMutationState: types.maybeNull(types.enumeration<QueryState>('QueryState', Object.values(QueryState))),

      // Error message in case createMutationState == error
      createMutationError: types.maybeNull(types.string),

      // Current state of the delete() call
      deleteMutationState: types.maybeNull(types.enumeration<QueryState>('QueryState', Object.values(QueryState))),

      // Error message in case deleteMutationState == error
      deleteMutationError: types.maybeNull(types.string),

      // Current state of the associate() call
      associateMutationState: types.maybeNull(types.enumeration<QueryState>('QueryState', Object.values(QueryState))),

      // Error message in case associateMutationState == error
      associateMutationError: types.maybeNull(types.string),

      // Set for field models of relations to mark whether any of the field model's relations can be unassociated (eg. by teh curent user)
      canUnassociateAny: types.optional(types.boolean, false),

      // Current state of the unassociate() call
      unassociateMutationState: types.maybeNull(types.enumeration<QueryState>('QueryState', Object.values(QueryState))),

      // Error message in case associateMutationState == error
      unassociateMutationError: types.maybeNull(types.string),

      // Elements of the current page
      pageResult: types.optional(types.array(types.reference(modelConfig.handledModel)), []),

      // ID of the entity to edit. loadedItem will contain its loaded form
      editId: types.maybeNull(types.union(types.number, types.string)),

      // ID of and entity selected in a list. Used when displaying details of the selected entity
      selectedId: types.maybeNull(types.union(types.number, types.string)),

      // Default field values which will be used in create() besides the passed params
      // Eg in case of many-to-one or one-to-one relationships defaultCreateFieldValues should
      // contian the ID of the owning field, to which the new instance will be assoicated upon
      // creation
      defaultCreateFieldValues: types.maybeNull(types.frozen<{ [key: string]: any }>()),

      // Default field values which will be used in update() besides the passed params
      defaultUpdateFieldValues: types.maybeNull(types.frozen<{ [key: string]: any }>()),

      // Current state of the item loading process
      loadQueryState: types.maybeNull(types.enumeration<QueryState>('QueryState', Object.values(QueryState))),

      // Error message in case loadQueryState == error
      loadQueryError: types.maybeNull(types.string),

      // Currently loaded item for view or editing
      loadedItem: types.maybeNull(types.reference(modelConfig.handledModel)),

      // Items selected in the EntityTable. Contians 1 item in case SINGLE selection and multiple items in case of MULTIPLE selection
      selectedItems: types.optional(types.array(types.reference(modelConfig.handledModel)), []),

      // Created objects of this type
      createdItem: types.maybeNull(types.reference(modelConfig.handledModel)),

      // Created objects of this type
      updatedItem: types.maybeNull(types.reference(modelConfig.handledModel)),

      // These should be overriden by actual entity models to provide models for their related values
      relations: types.optional(types.model('Relations', {}), {}),
    })
    .actions((self) => ({
      getParentField(field: string) {
        let depth = 1
        // must use any, otherwise typing blows up for parent related functions ...
        let parent: any = getParent(self, depth)
        try {
          do {
            if (parent[field]) {
              return parent[field] as any
            }
            depth++
          } while ((parent = getParent(self, depth)))
        } catch (e) {
          const msg = `Unable to find ${field} in parents until depth ${depth}. Does you root store have a ${field}?`
          console.log(msg + ' Started looking from self', self)
          throw new Error(msg)
        }
      },
    }))
    .actions((self) => {
      // Save initial state, so that we can reset it when signing out
      let initialState = {}
      const actions = {
        afterCreate: () => {
          initialState = getSnapshot(self)
        },
        reset: () => {
          applySnapshot(self, initialState)
        },
      }
      return actions
    })
    .views((self) => ({
      get numberOfPages() {
        return self.count ? Math.ceil(self.count / self.pageSize) : 0
      },

      get gqlStore(): GqlStoreType {
        return self.getParentField('gqlStore')
      },
      /**
       * Calculates the final where condition from self.baseWhere and self.where
       */
      get finalWhere(): BoolExpType {
        if (!self.baseWhere && !self.where) {
          return {} as BoolExpType
        }
        if (!self.baseWhere && self.where) {
          return self.where
        }
        if (self.baseWhere && !self.where) {
          return self.baseWhere
        }
        // Otherwise AND the two conditions
        const res: BoolExpType = {} as BoolExpType
        res['_and'] = [self.baseWhere, self.where]
        return res
      },
    }))
    //
    // Overridable properties, functions
    //
    .views((/* self */) => ({
      // Overriden by models via relation() acting as related models to a rootEntityModel entity
      // @ts-ignore
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      configureRelated(rootEntityModel: any, params: RouteConfig) {},

      // Overriden via relation()
      // In case it describes a related entity, this is the field name of the relation
      get relatedFieldName() {
        return null
      },

      // In case of relation, this is the root model
      get rootEntityModel() {
        return null
      },

      // In case of relation, this is the root model's name
      get rootEntityModelName() {
        return null
      },

      // May be overriden by EntityModels to customize it
      // model is actuall an EntityModelType byt need to use any to avoid recursion
      createEntityNavigator(model: any) {
        const nav = createEntityNavigator(model) as React.ComponentType
        return nav
      },
    }))
    .actions((self) => ({
      /**
       * Internal. Calculate the number of all elements in list
       * Moved out of the ain actions so that it can be called as
       * yield self._count() otherwise typing of the model blows up
       */
      _count() {
        const query = self.gqlStore.query(
          `query {
              ${modelConfig.handledModelBaseName}Aggregate(
                where: ${queryfy(self.finalWhere)},
              ) {
                aggregate {
                  count
                }
              }
            }`
        )
        return query
      },
    }))
    .views((self) => {
      let theConfig = modelConfig
      theConfig.setEntityModel(self as any)
      return {
        get config() {
          return theConfig
        },

        setConfig(newConfig: EntityConfigAny) {
          theConfig = newConfig
          theConfig.setEntityModel(self as any)
        },
      }
    })
    .actions((self) => {
      const actions = {
        setPageSize(pageSize: number) {
          self.pageSize = pageSize
          self.currentPage = 0
        },

        /**
         * Sets the query parameters for listing ServiceAccounts
         * @param where conditions for querying
         * @param orderBy sort specification for ordering result
         */
        setQueryParams(where: BoolExpType | null, orderBy: OrderByType[] | null) {
          self.count = null
          // @ts-ignore
          self.where = where
          self.orderBy = orderBy || (defaultOrderBy as unknown as OrderByType[])
        },

        setBaseQueryCondition(where: BoolExpType | null) {
          // @ts-ignore
          self.baseWhere = where
        },

        /**
         * Internal. Set count of all elements we are about to display.
         * @param count
         */
        _setCount(count: number | null) {
          self.count = count
        },

        setListQueryState(state: QueryState, error?: string) {
          self.listQueryState = state
          self.listQueryError = error
        },

        setPageResult(pageResult: Array<ModelType>) {
          // console.log("*** setPageResult", JSON.stringify(pageResult))
          self.pageResult.replace(pageResult)
        },

        reloadList() {
          // When list is empty fake 1 item, to force reaction
          if (self.count == null) {
            actions._setCount(1)
          }
          // Otheriwse clear count to force reaction
          else {
            actions._setCount(null)
          }
          actions.setPageResult([])
        },

        setLoadedItem(item?: ModelType) {
          self.loadedItem = item
        },

        addSelectedItem(item: ModelType) {
          self.selectedItems.push(item)
        },

        removeSelectedItem(item: ModelType) {
          self.selectedItems.replace(self.selectedItems.filter((e) => e.id != (item as any).id))
        },

        clearSlectedItems() {
          self.selectedItems.clear()
        },

        isSelectedItem(item: ModelType) {
          return self.selectedItems.find((e) => e.id == (item as any).id)
        },

        setCreatedItem(item?: ModelType) {
          self.createdItem = item
        },

        setUpdatedItem(item?: ModelType) {
          // console.log("+++ setUpdatedItem ", item)
          self.updatedItem = item
        },

        setEditId(id: string | number | null = null) {
          self.editId = id
        },

        setSelectedId(id: string | number | null = null) {
          self.selectedId = id
        },

        /**
         * List elements on the given page
         * @param page page to display
         */
        // @ts-ignore
        list: flow(function* (page: number) {
          try {
            // Typings? https://github.com/mikecann/mst-flow-pipe
            self.listQueryState = QueryState.PENDING
            if (self.count == null) {
              const result = yield self._count()
              self.count = result[modelConfig.handledModelBaseName + 'Aggregate'].aggregate.count
              self.currentPage = 0
            } else {
              self.currentPage = page
            }

            // Make sure we have a default ordering by ID
            if (self.orderBy.length == 0) {
              self.orderBy = defaultOrderBy as unknown as OrderByType[]
            }

            // we have a JSON structure in self.where and self.orderBy and convert these to graphql variable values.
            // note: for orderBy "asc" and "desc" are strings and will become strings in the queryfy()'ed version as well,
            // but we need graphql enum values, so we replace the quotation marks in post process.
            const query = self.gqlStore.query(
              `
               query {
                ${modelConfig.listOperation}(
                  where: ${queryfy(self.finalWhere)}
                  order_by: ${
                    self.orderBy ? queryfy(self.orderBy).replace('"asc"', 'asc').replace('"desc"', 'desc') : '[]'
                  }
                  limit: ${self.pageSize}
                  offset: ${self.currentPage * self.pageSize}
                ) {
                  ${modelConfig.listOperationSelectionSet}
                }
               }
              `,
              {},
              {
                // When doing initial fetch, make sure we don't use the cache
                fetchPolicy: self.pageResult.length == 0 ? 'network-only' : 'cache-and-network',
              }
            )
            query.then(
              (result: any) => {
                actions.setListQueryState(QueryState.DONE)
                actions.setPageResult(result[modelConfig.listOperation])
              },
              (error) => {
                actions.setListQueryState(QueryState.ERROR, error)
              }
            )
            return query
          } catch (error) {
            // ... including try/catch error handling
            console.error('list() failed', error)
            throw error
          }
        }),

        setLoadQueryState(state: QueryState, error: string | null = null) {
          self.loadQueryState = state
          self.loadQueryError = error
        },

        load(id: string) {
          actions.setLoadQueryState(QueryState.PENDING)
          const query = self.gqlStore.query(
            `
              query {
                ${modelConfig.loadOperation}(
                  id: ${id}
                ) {
                  ${modelConfig.loadOperationSelectionSet}
                }
              }
            `,
            {},
            {
              fetchPolicy: 'network-only',
            }
          )
          query.then(
            (result: any) => {
              actions.setLoadQueryState(QueryState.DONE)
              actions.setLoadedItem(result[modelConfig.loadOperation])
            },
            (error) => {
              actions.setLoadQueryState(QueryState.ERROR, error)
            }
          )
          return query
        },

        setDefaultCreateFieldValues(values: { [key: string]: any }) {
          self.defaultCreateFieldValues = values
        },

        setDefaultUpdateFieldValues(values: { [key: string]: any }) {
          self.defaultUpdateFieldValues = values
        },

        setUpdateMutationState(state: QueryState, error: string | null = null) {
          self.updateMutationState = state
          self.updateMutationError = error
        },

        update(params: UpdateParamType) {
          actions.setUpdateMutationState(QueryState.PENDING)
          const allParams = self.defaultUpdateFieldValues ? { ...self.defaultUpdateFieldValues, ...params } : params
          const mut = modelConfig.updateMutation!(self.gqlStore, self.loadedItem, allParams)
          mut.then(
            (result: any) => {
              // If we have an explicit path, use that
              if (modelConfig.updateMutationResultPath) {
                actions.setUpdatedItem(getPath(modelConfig.updateMutationResultPath!, result)!)
              }
              // Otherwise just use the first key of the object, which is usually what we want anyway
              else {
                actions.setUpdatedItem(result[Object.keys(result)[0]])
              }
              actions.setUpdateMutationState(QueryState.DONE)
            },
            (error) => {
              actions.setUpdateMutationState(QueryState.ERROR, error)
            }
          )

          return mut
        },

        setCreateMutationState(state: QueryState, error: string | null = null) {
          self.createMutationState = state
          self.createMutationError = error
        },

        create(params: CreateParamType) {
          actions.setCreateMutationState(QueryState.PENDING)
          const allParams = self.defaultCreateFieldValues ? { ...self.defaultCreateFieldValues, ...params } : params
          const mut = modelConfig.createMutation!(self.gqlStore, allParams)
          mut.then(
            (result: any) => {
              actions._setCount(null)
              actions.setPageResult([])
              // If we have an explicit path, use that
              if (modelConfig.createMutationResultPath) {
                actions.setCreatedItem(getPath(modelConfig.createMutationResultPath!, result)!)
              }
              // Otherwise just use the first key of the object, which is usually what we want anyway
              else {
                actions.setCreatedItem(result[Object.keys(result)[0]])
              }
              actions.setCreateMutationState(QueryState.DONE)
            },
            (error) => {
              actions.setCreateMutationState(QueryState.DONE, error)
              throw error
            }
          )
          return mut
        },

        setAssociateMutationState(state: QueryState, error: string | null = null) {
          self.associateMutationState = state
          self.associateMutationError = error
        },

        associate(relation: any, associateIds: string[] | number[]) {
          actions.setAssociateMutationState(QueryState.PENDING)
          // let mut = relation.associate
          //   ? relation.associate(self.gqlStore, self.loadedItem, relation.fieldName, associateIds)
          //   : modelConfig.associateMutation(self.gqlStore, self.loadedItem, relation.fieldName,  associateIds)
          let mut = relation.associate(self.gqlStore, self.loadedItem, relation.fieldName, associateIds)
          // May handle result/error here
          mut.then(
            (/* result */) => {
              actions.setAssociateMutationState(QueryState.DONE)
            },
            (error) => {
              actions.setAssociateMutationState(QueryState.ERROR, error)
              throw error
            }
          )
          return mut
        },

        setCanUnassociateAny(flag: boolean) {
          self.canUnassociateAny = flag
        },

        setUnassociateMutationState(state: QueryState, error: string | null = null) {
          self.unassociateMutationState = state
          self.unassociateMutationError = error
        },

        unassociate(relation: any, associateIds: string[] | number[]) {
          actions.setUnassociateMutationState(QueryState.PENDING)
          const mut = relation.unassociate(self.gqlStore, self.loadedItem, relation.fieldName, associateIds)
          mut.then(
            (/*result: any*/) => {
              actions.setUnassociateMutationState(QueryState.DONE)
            },
            (error) => {
              actions.setUnassociateMutationState(QueryState.ERROR, error)
              throw error
            }
          )
          return mut
        },

        setDeleteMutationState(state: QueryState, error: string | null = null) {
          self.deleteMutationState = state
          self.deleteMutationError = error
        },

        delete(items: ModelType[]) {
          actions.setDeleteMutationState(QueryState.PENDING)
          const mut = modelConfig.deleteMutation!(self.gqlStore, items)
          mut.then(
            (/*result*/) => {
              actions.setDeleteMutationState(QueryState.DONE)
            },
            (error) => {
              actions.setDeleteMutationState(QueryState.ERROR, error)
              throw error
            }
          )
          return mut
        },

        // TODO: remove, not used. Using setDefaultCreateFieldValues and setDefaultUpdateFieldValues instead
        setJoinFields(joinFields: { [key: string]: any }) {
          self.joinFields = joinFields
        },
      }
      return actions
    })

  // Configure related if there's relations config on modelConfig
  if (modelConfig.relations && modelConfig.ignoreRelations !== true) {
    let props = {}
    modelConfig.relations().forEach((r) => {
      props[r.fieldName] = relation(model, r)
    })
    const RelationsModel = types.model('Relations', props)

    // Override the original relations field of EntityModel with the actually defined RelationsModel
    model = model.props({
      relations: initialized(RelationsModel),
    })
  }

  return model
}

/**
 * Sets `config` as the configuration of the given `model`.
 * @param model
 * @param config
 */
// export function configured(model: EntityModel, config: EntityConfigAny) {
//   return model.views((self) => {
//     config.setEntityModel(self as any)
//     return {
//       get config() {
//         return config
//       },
//     }
//   })
// }
/**
 * Queryfy.
 *
 * Prep javascript objects for interpolation within graphql queries.
 *
 * @param {mixed} obj
 * @return template string.
 */
const queryfy = (obj) => {
  // Make sure we don't alter integers.
  if (typeof obj === 'number') {
    return obj
  }

  if (Array.isArray(obj)) {
    const props = obj.map((value) => `${queryfy(value)}`).join(',')
    return `[${props}]`
  }

  if (typeof obj === 'object') {
    const props = Object.keys(obj)
      .map((key) => `${key}:${queryfy(obj[key])}`)
      .join(',')
    return `{${props}}`
  }

  return JSON.stringify(obj)
}

// export function one2m(args: { fieldName: string; fieldModel: EntityModel; assocModel: EntityModel }) {
//   const { fieldName, fieldModel, assocModel } = args
//   const One2MModel = types.model('One2MRelationModel', {
//     fieldName: types.optional(types.string, fieldName),
//     fieldModel: initialized(fieldModel),
//     assocModel: initialized(
//       assocModel.views(() => ({
//         get one2mFieldName() {
//           return fieldName
//         },
//       }))
//     ),
//   })
//   return initialized(One2MModel)
// }

/**
 * Convenience function to declare types.optional(someType {}) --> initialized(someType)
 * @param type
 */
export function initialized<IT extends IAnyType>(type: IT): IOptionalIType<IT, [undefined]> {
  return types.optional(type, {})
}

export interface RelationModelConfig {
  fieldName: string
  fieldModel: EntityModel
  fieldModelConfigurator: (fieldModel: EntityModelType, rootEntityModel: EntityModelType, params: RouteConfig) => void
  assocModel?: EntityModel
  assocModelConfigurator?: (assocModel: EntityModelType, rootEntityModel: EntityModelType, params: RouteConfig) => void
  associate?: (gqlStore: StoreType, loadedItem: any, field: string, associatedIds: string[]) => Query
  unassociate?: (gqlStore: StoreType, loadedItem: any, field: string, associatedIds: string[]) => Query
  canUnassociate?: (item: any, viewType: ViewType, fieldModel: EntityModelType) => boolean
  allowCreatingNewRelated?: boolean
  assocSelectionType?: AssocSelectionType
  // Called to make changed to the form values before doing the actual mutation.
  prepareNew?: (
    gqlStore: StoreType,
    rootEntityModel: EntityModelType,
    model: EntityModelType,
    formValues: any,
    field: string
  ) => void
  // Not supported yet. Would be used to execute creation mutation in its own. A createNew() implementation should eventually call model.create().
  createNew?: (
    gqlStore: StoreType,
    rootEntityModel: EntityModelType,
    model: EntityModelType,
    formValues: any,
    field: string
  ) => Query
}

/**
 * Defines a RelationModel for a specific field of a root model.
 * @param args relation arguments, see below:
 * @param args.fieldName  name of the relationship field
 * @param args.fieldModel  model to display currently set related values
 * @param args.fieldModelConfigurator  function to configure the fieldModel instance at runtime
 * @param args.assocModel  the model used for displaying entities which ar associable for this relation. It must be the same type model as fieldModel
 * but configured for displaying associaable entities.
 * @param args.assocModelConfigurator  function to configure the assocModel instance at runtime
 * @param args.associate function to do the association between the entities selected in assocModel and the root model
 * @param args.associate function to do the unassociation between the entities selected in assocModel and the root model
 * @param args.allowCreatingNewRelated if true, displays a form for creating a new associable entity and assign it with the root model. Default: false
 * @param args.assocSelectionType how many items can be associated from the assocModel? Default to NONE, in which case specifying assocModel is * not necessary
 * @param args.prepareNew called to allow making changed to the form values before doing the actual mutation
 * @param args.prepareNew Not supported yet. Would be used to execute creation mutation in its own. This is now done by EntityForm using
 * model.create(). A createNew() implementation should eventually call rootModel.create() as well.
 */
export function relation(rootEntityModel: EntityModel, args: RelationModelConfig) {
  const {
    fieldName,
    fieldModel,
    fieldModelConfigurator,
    assocModel,
    assocModelConfigurator,
    associate,
    unassociate,
    allowCreatingNewRelated,
    prepareNew,
    createNew,
    assocSelectionType,
    canUnassociate,
  } = args

  let relModel = types
    .model('RelationModel', {
      rootEntityModelName: types.maybe(types.string),
      fieldName: types.optional(types.string, args.fieldName),
      fieldModel: initialized(
        fieldModel.views((self) => ({
          configureRelated(rootEntityModel: any, params: RouteConfig) {
            fieldModelConfigurator(self, rootEntityModel, params)
          },

          get relatedFieldName() {
            return fieldName
          },

          get rootEntityModel() {
            return rootEntityModel
          },

          get rootEntityModelName() {
            return rootEntityModel.name
          },
        }))
      ),
    })
    .views(() => ({
      get allowCreatingNewRelated() {
        return typeof allowCreatingNewRelated === 'undefined' ? false : allowCreatingNewRelated
      },

      get assocSelectionType() {
        return typeof assocSelectionType === 'undefined' ? AssocSelectionType.NONE : assocSelectionType
      },

      // These are properties returning functions, so effectivelu these can be called as assocModel.associate(gqlStore, loadedItem, ...)
      get associate() {
        return associate
      },

      get unassociate() {
        return unassociate
      },

      get canUnassociate() {
        return canUnassociate
      },

      get prepareNew() {
        return prepareNew
      },

      get createNew() {
        return createNew
      },
    }))

  // assocModel is optional. Also see RelationModelType where it is added explicitly since adding it like this to props
  // won't be seen in the typings.
  if (assocModel) {
    relModel = relModel.props({
      assocModel: initialized(
        assocModel.views((self) => ({
          configureRelated(rootEntityModel: any, params: RouteConfig) {
            const c = self.config.entityListScreenConfig!
            // Configure the table selection mode. Note: assocModelConfigurator may override it
            switch (assocSelectionType) {
              case AssocSelectionType.MULTIPLE:
                c.selectionType = TableSelectionType.MULTIPLE
                break
              case AssocSelectionType.NONE:
              case AssocSelectionType.SINGLE:
                c.selectionType = TableSelectionType.SINGLE
                break
            }
            assocModelConfigurator!(self, rootEntityModel, params)
          },

          get relatedFieldName() {
            return fieldName
          },

          get rootEntityModel() {
            return rootEntityModel
          },

          get rootEntityModelName() {
            return rootEntityModel.name
          },
        }))
      ),
    })
  }

  return initialized(relModel)
}

/**
 * True if model is of type "RelationModel", ie. one that is created using the `relation()` function.
 * @param model
 */
export function isRelation(model: IAnyModelType) {
  return getType(model).name == 'RelationModel'
}

export type RelationModelType = Instance<ReturnType<typeof relation>> & {
  assocModel?: EntityModelType
}

export type EntityModel = ReturnType<typeof createEntityModel>
export type EntityModelType = Instance<EntityModel>

/**
 * Creates stack navigator and defines screen for an EntityModelType with possible additional
 * screens generated by additionalScreens
 * @param rootEntityModel
 * @param additionalScreens
 */
export function createEntityNavigator(
  rootEntityModel: EntityModelType,
  initalRouteName?: string,
  additionalScreens?: (
    entityModel: EntityModelType,
    Stack: ReturnType<typeof createNativeStackNavigator>,
    props: React.PropsWithChildren<NativeStackScreenProps<any, string>>
  ) => JSX.Element
) {
  // const rootName = getType(rootEntityModel).name
  const Stack = createNativeStackNavigator()

  const entityNavigator: React.FC<NativeStackScreenProps<any>> = observer((props) => {
    // Note: for every screen we expect the props.route.params.title to provide the route title, we don't provide a default one here
    return (
      <Stack.Navigator initialRouteName={initalRouteName}>
        {rootEntityModel.config.listScreen !== null && (
          // @ts-ignore
          <Stack.Screen
            component={rootEntityModel.config.listScreenVal()}
            name={rootEntityModel.config.listScreenRouteNameVal()}
            options={(props: any) => ({
              title: props.route.params.title,
            })}
            // Pass on props.route.params as initialParams. Ie. the initialParams from the
            // Menu Drawer navigator passing the rootEntityModel.
            initialParams={{
              ...props.route.params,
            }}
          />
        )}
        {rootEntityModel.config.detailsScreen !== null && (
          // @ts-ignore
          <Stack.Screen
            component={rootEntityModel.config.detailsScreenVal()}
            name={rootEntityModel.config.detailsScreenRouteNameVal()}
            options={(props: any) => ({
              title: props.route.params.title,
            })}
            // options={props => ({
            //   title: listScreenTitle(rootEntityModel.config)
            // })}
            // Pass on props.route.params as initialParams. Ie. the initialParams from the
            // Menu Drawer navigator passing the rootEntityModel.
            initialParams={{
              ...props.route.params,
            }}
          />
        )}
        {rootEntityModel.config.createFormScreen !== null && (
          // @ts-ignore
          <Stack.Screen
            component={rootEntityModel.config.createFormScreenVal()}
            name={rootEntityModel.config.createFormScreenRouteNameVal()}
            options={(props: any) => ({
              title: props.route.params.title,
            })}
            // options={props => ({
            //   title: listScreenTitle(rootEntityModel.config)
            // })}
            // Pass on props.route.params as initialParams. Ie. the initialParams from the
            // Menu Drawer navigator passing the rootEntityModel.
            initialParams={{
              ...props.route.params,
            }}
          />
        )}
        {rootEntityModel.config.editFormScreen !== null && (
          // @ts-ignore
          <Stack.Screen
            component={rootEntityModel.config.editFormScreenVal()}
            name={rootEntityModel.config.editFormScreenRouteNameVal()}
            options={(props: any) => ({
              title: props.route.params.title,
            })}
            // Pass on props.route.params as initialParams. Ie. the initialParams from the
            // Menu Drawer navigator passing the rootEntityModel.
            initialParams={{
              ...props.route.params,
            }}
          />
        )}
        {/* Assoc screen for any associations */}
        {/*// @ts-ignore*/}
        <Stack.Screen
          component={rootEntityModel.config.assocScreenVal()}
          name={rootEntityModel.config.assocScreenRouteNameVal()}
          //assocM2MRouteTitle
          options={(props: any) => ({
            title: props.route.params.title, // ? props.route.params.title : assocScreenRouteTitle(rootEntityModel.config)
          })}
          // Pass on props.route.params as initialParams. Ie. the initialParams from the
          // Menu Drawer navigator passing the rootEntityModel.
          initialParams={{
            ...props.route.params,
          }}
        />
        {/*{Object.entries(rootEntityModel.relations)*/}
        {/*  .map(arr => {*/}
        {/*    const key = arr[0]*/}
        {/*    const m = arr[1]*/}
        {/*    let relation = m as RelationModelType*/}
        {/*    let relatedModel = relation.fieldModel*/}
        {/*    if (relation.assocSelectionType == AssocSelectionType.NONE) {*/}
        {/*      return (*/}
        {/*        <>*/}
        {/*          <Stack.Screen*/}
        {/*            key={key+"-1"}*/}
        {/*            component={relatedModel.config.createFormScreenVal()}*/}
        {/*            name={rootName+relatedModel.config.createFormScreenRouteNameVal()}*/}
        {/*            options={(props: any) => ({*/}
        {/*              title: props.route.params.title*/}
        {/*            })}*/}
        {/*            // Pass on props.route.params as initialParams. Ie. the initialParams from the*/}
        {/*            // Menu Drawer navigator passing the rootEntityModel.*/}
        {/*            initialParams={{*/}
        {/*              ...props.route.params*/}
        {/*            }}*/}
        {/*          />*/}
        {/*          <Stack.Screen*/}
        {/*            key={key+"-2"}*/}
        {/*            component={relatedModel.config.editFormScreenVal()}*/}
        {/*            name={rootName+relatedModel.config.editFormScreenRouteNameVal()}*/}
        {/*            options={(props: any) => ({*/}
        {/*              title: props.route.params.title*/}
        {/*            })}*/}
        {/*            // Pass on props.route.params as initialParams. Ie. the initialParams from the*/}
        {/*            // Menu Drawer navigator passing the rootEntityModel.*/}
        {/*            initialParams={{*/}
        {/*              ...props.route.params*/}
        {/*            }}*/}
        {/*          />*/}
        {/*        </>*/}
        {/*      )*/}
        {/*    }*/}
        {/*    else {*/}
        {/*      const assocModel = relation.fieldModel*/}
        {/*      return (*/}
        {/*        <Stack.Screen*/}
        {/*          key={key}*/}
        {/*          component={assocModel.config.assocScreenVal()}*/}
        {/*          name={rootName+assocModel.config.assocScreenRouteNameVal()}*/}
        {/*          //assocM2MRouteTitle*/}
        {/*          options={(props: any) => ({*/}
        {/*            title: props.route.params.title// ? props.route.params.title : assocScreenRouteTitle(rootEntityModel.config)*/}
        {/*          })}*/}
        {/*          // Pass on props.route.params as initialParams. Ie. the initialParams from the*/}
        {/*          // Menu Drawer navigator passing the rootEntityModel.*/}
        {/*          initialParams={{*/}
        {/*            ...props.route.params*/}
        {/*          }}*/}
        {/*        />*/}
        {/*      )*/}
        {/*    }*/}
        {/*  })*/}
        {/*}*/}
        {additionalScreens && additionalScreens(rootEntityModel, Stack, props)}
      </Stack.Navigator>
    )
  })

  return entityNavigator
}

export class FieldConfigValues<ModelType> {
  // In case the field is virtual, ie. it is not present right on the entity, but in a referred entity, then this dot notated path
  // declares how the value can be accessed. Ie. If we list a Profile entity and the email is contained in a joined User entity, then
  // the accessor will be "user.email". This will be used both for accessing and sorting the field.
  name!: string

  // default: same as 'name'. Can also be a function to provide a display name dynamically, eg. according to the current localization
  displayName?: string | ((config: FieldConfig<ModelType>, viewType: ViewType, item?: any) => string)

  // default: false. TODO: acquire default from the model nad allow overriding it here
  numeric?: boolean | ((config: FieldConfig<ModelType>) => boolean)

  // default: true
  listable?: boolean | ((config: FieldConfig<ModelType>) => boolean)

  // default: true
  listableInDetails?: boolean | ((config: FieldConfig<ModelType>) => boolean)

  // default: false
  searchable?: boolean | ((config: FieldConfig<ModelType>) => boolean)

  // field to actually search for when searching
  searchAccessor?: string | ((config: FieldConfig<ModelType>) => string)

  // default: true
  sortable?: boolean | ((config: FieldConfig<ModelType>) => boolean)

  // field to actually sort when this field is selected for sorting
  sortAccessor?: string | ((config: FieldConfig<ModelType>) => string)

  // default: true, editable fields appear in forms
  editable?: boolean | ((config: FieldConfig<ModelType>) => boolean)

  // If value is not acessible using the "name" property above when getting its value from the loadedItem in the model
  // then use this accessor string. This is uses in cases when the value comes from a joined entity, eg.
  // members.0.firstName, which will access loadedItem.members[0].
  fieldValueAccessor?: string | ((config: FieldConfig<ModelType>) => string)

  // List of roles for which this field is allowed to be shown. If neither includeForRoles and excludeForRoles
  // specified, it will be shown for all roles. If both includeForRoles and excludeForRoles is specified,]
  // the includeForRoles has precedence. Ie. if a role is present in both includeForRoles and excludeForRoles
  // includeForRoles will have precedence and so the field will be included.
  includeForRoles?: string[] | ((config: FieldConfig<ModelType>, item?: ModelType) => string[])

  // List of roles for which this field should be hidden
  excludeForRoles?: string[] | ((config: FieldConfig<ModelType>, item?: ModelType) => string[])

  // Calculated at runtime and shows whether the field should be displayed in the current contex. For now it is
  // afffected by the current role of the user and the settings of  includeForRoles and excludeForRoles
  _hidden?: boolean

  // When accessing via name or fieldValueAccessor is not enough, you can provide a function to get the field
  // value from item.
  fieldValue?: (config: FieldConfig<ModelType>, viewType: ViewType, item: ModelType) => string

  // default: true, editable fields appear in forms
  editableAtCreation?: boolean | ((config: FieldConfig<ModelType>) => boolean)

  // default: false. Even if not editable, it maybe displayed in a form if set to false
  viewInForm?: boolean | ((config: FieldConfig<ModelType>) => boolean)

  // default: false. If editableAtCreation is false, but want to show its value in the creation form.
  // eg. in case of associated fields with a preset association value like TodoList.locationId
  viewInFormAtCreation?: boolean | ((config: FieldConfig<ModelType>) => boolean)

  // Yup validation of the field for creation/editing
  validation?: BaseSchema | ((config: FieldConfig<ModelType>) => BaseSchema)
  validationCyclic?: [string, string] | ((config: FieldConfig<ModelType>) => [string, string])
  createValidation?: BaseSchema | ((config: FieldConfig<ModelType>) => BaseSchema)
  editValidation?: BaseSchema | ((config: FieldConfig<ModelType>) => BaseSchema)

  // Type of input to use for this field
  inputType?: InputType | ((config: FieldConfig<ModelType>, item?: ModelType) => InputType)

  // In case of inputType="select", you must provide selectValues for selecting a value for the field.
  // In SelectValues the key will be the actual value of stored in the field and the value will be the
  // value displayed in the SelectControl
  selectValues?: SelectValues | ((config: FieldConfig<ModelType>, item?: ModelType) => SelectValues)

  // If using the default table, just want to add some styling, config. If ViewConfig.tableComponent is specified, these are ignorred.
  tableProps?: {
    // default:1 . If want the column to take up more space set a higher value
    columnWidthFlex?: number | ((config: FieldConfig<ModelType>) => number)

    // to replace the default DataTable.Title with own component
    titleComponent?: (config: FieldConfig<ModelType>) => Component

    // If using default DataTable.Title add these styled props
    titleProps?: StyledProps | ((config: FieldConfig<ModelType>) => StyledProps)

    // to replace the default DataTable.Title with own component
    cellComponent?: (config: FieldConfig<ModelType>, item: ModelType) => Component

    // If using default DataTable.Cell add these styled props
    cellProps?: StyledProps | ((config: FieldConfig<ModelType>, item: ModelType) => StyledProps)

    // If using default DataTable.Cell add these styled props
    textProps?: StyledProps | ((config: FieldConfig<ModelType>, item: ModelType) => StyledProps)
  }

  // TODO: rename to inForm
  inForm?: {
    inputComponent?: (
      config: FieldConfig<ModelType>,
      item: ModelType,
      validationSchema: ObjectSchema<any>
    ) => Component<any>
  }

  // TODO: rename to inData
  inData?: {
    cellComponent?: (config: FieldConfig<ModelType>, item: ModelType, model: any) => Component<any>
  }

  assocSingle?: {
    // The entity model key in Appstrore.associableModels
    modelKey: string | ((config: FieldConfig<ModelType>, item: ModelType) => string)

    // The screen, which actually displays the association table
    screenRoute: string | ((config: FieldConfig<ModelType>, item: ModelType) => string)

    // Field value when editing in a Form an associated field value
    assocFieldValue: (
      config: FieldConfig<ModelType>,
      viewType: ViewType,
      item: ModelType,
      formValues: any,
      gqlStore: StoreType
    ) => string

    // Base where cndition based on the current item and the current form values. Ie. a form value already set may affect the
    // list of associable values
    baseWhere?: object | ((config: FieldConfig<ModelType>, item: ModelType, formValues: any) => any)
  }

  constructor(args: FieldConfigValues<ModelType>) {
    Object.keys(args).forEach((key) => (this[key] = args[key]))
  }
}

export class FieldConfig<ModelType> extends FieldConfigValues<ModelType> {
  valueOrDefault<T, ModelType>(
    val: T | ((config: FieldConfig<ModelType>, item: any) => T),
    def: T,
    fieldConfig: FieldConfig<ModelType>,
    item?: any
  ): T {
    if (val === null || val === undefined) {
      return def
    }
    if (typeof val === 'function') {
      // @ts-ignore: FieldConfig and ViewConfig don't match because of name/title, but we know we will call the
      // right type of function here
      const res = (val as (config: FieldConfig<ModelType>, any) => T)(fieldConfig, item)
      // If return value is undefined, it means we need to use the default value
      if (typeof res !== 'undefined') {
        return res
      }
      return def
    }
    return val
  }

  displayNameVal(viewType: ViewType = ViewType.RAW, item?: any) {
    const val = this.displayName
    if (val === null || val === undefined) {
      return this.name
    }
    if (typeof val === 'function') {
      // @ts-ignore: FieldConfig and ViewConfig don't match because of name/title, but we know we will call the
      // right type of function here
      return (val as (config: FieldConfig<ModelType>, viewType, item?) => T)(this, viewType, item)
    }
    return val
  }

  numericVal() {
    return this.valueOrDefault(this.numeric, false, this)
  }
  listableVal() {
    return this.valueOrDefault(this.listable, true, this) && !this.hiddenVal()
  }
  listableInDetailsVal() {
    return this.valueOrDefault(this.listableInDetails, true, this) && !this.hiddenVal()
  }
  searchableVal() {
    return this.valueOrDefault(this.searchable, false, this)
  }
  searchAccessorVal() {
    return this.valueOrDefault(this.searchAccessor, this.name, this)
  }
  sortableVal() {
    return this.valueOrDefault(this.sortable, true, this)
  }
  sortAccessorVal() {
    return this.valueOrDefault(this.sortAccessor, this.name, this)!
  }
  editableVal() {
    return this.valueOrDefault(this.editable, true, this) && !this.hiddenVal()
  }
  fieldValueAccessorVal() {
    return this.valueOrDefault(this.fieldValueAccessor, null, this)
  }
  editableAtCreationVal() {
    return this.valueOrDefault(this.editableAtCreation, true, this) && !this.hiddenVal()
  }
  viewInFormVal() {
    return this.valueOrDefault(this.viewInForm, false, this) && !this.hiddenVal()
  }
  viewInFormAtCreationVal() {
    return this.valueOrDefault(this.viewInFormAtCreation, false, this) && !this.hiddenVal()
  }
  validationVal() {
    return this.valueOrDefault(this.validation, null, this)
  }
  validationCyclicVal() {
    return this.valueOrDefault(this.validationCyclic, null, this)
  }
  editValidationVal() {
    return this.valueOrDefault(this.editValidation, null, this)
  }
  createValidationVal() {
    return this.valueOrDefault(this.createValidation, null, this)
  }
  columnWidthFlexVal() {
    return this.valueOrDefault(this.tableProps?.columnWidthFlex, 1, this)
  }
  titleComponentVal() {
    return this.valueOrDefault(this.tableProps?.titleComponent, null, this)
  }
  titlePropsVal() {
    return this.valueOrDefault(this.tableProps?.titleProps, null, this)
  }
  cellComponentVal() {
    return this.valueOrDefault(this.tableProps?.cellComponent, null, this)
  }
  cellPropsVal(item: any) {
    return this.valueOrDefault(this.tableProps?.cellProps, null, this, item)
  }
  textPropsVal(item: any) {
    return this.valueOrDefault(this.tableProps?.textProps, null, this, item)
  }
  inputTypeVal(item?: any) {
    return this.valueOrDefault(this.inputType, 'text', this, item)
  }
  selectValuesVal(item?: any) {
    return this.valueOrDefault(this.selectValues, null, this, item)!
  }
  includeForRolesVal(item?: any) {
    return this.valueOrDefault(this.includeForRoles, [], this, item)
  }
  excludeForRolesVal(item?: any) {
    return this.valueOrDefault(this.excludeForRoles, [], this, item)
  }
  hiddenVal() {
    return this.valueOrDefault(this._hidden, false, this)
  }
  assocSingleModelKeyVal(item: any) {
    return this.valueOrDefault(this.assocSingle?.modelKey, null, this, item)!
  }
  assocSingleScreenRouteVal(item: any) {
    return this.valueOrDefault(this.assocSingle?.screenRoute, null, this, item)!
  }
  assocSingleBaseWhereVal(item: any, fieldValues: object) {
    if (!this.assocSingle) {
      return {}
    }
    const val = this.assocSingle.baseWhere
    if (val === null || val === undefined) {
      return {}
    }
    if (typeof val === 'function') {
      // @ts-ignore: FieldConfig and ViewConfig don't match because of name/title, but we know we will call the
      // right type of function here
      return (val as (config: FieldConfig<ModelType>, item?, fieldValues) => T)(this, item, fieldValues)
    }
    return val
  }
}

export type StoreType = Instance<typeof MSTGQLStore.Type>

export class EntityConfigValues<ModelType, UpdateParamType, CreateParamType, BoolExpType> {
  // Just for debuggign to see if two configs are actually different objects
  _randomId? = Math.random()

  // Name of the MST model
  modelName!: string

  // MTS model object
  handledModel!: IModelType<any, any> // typeof ModelBase,

  // Base name of the model, used for eg. aclculating the agggregate name.
  // IEg.. handledModelBaseName=serviceAccount --> aggregate = serviceAccountAggregate
  handledModelBaseName!: string // serviceAccount

  // Name of the GQL operation to list multiple items of this type. Most of the time this is the plural of handledModelBaseName
  listOperation!: string // serviceAccounts

  // GQL selection set specifying, which fields to return for a listOperation
  listOperationSelectionSet!: string // {id, createdAt, ...}

  // Name of GQL operation to load a single a object of this type either for displaying it or for editing.
  // Typically the same as handledModelBaseName
  loadOperation!: string // serviceAccount

  // GQL selection set specifying, which fields to return for a loadOperation
  loadOperationSelectionSet!: string // {id, createdAt, ...}

  // Mutation to update an object of this type
  updateMutation?: (gqlStore: StoreType, loadedItem: ModelType, params: UpdateParamType) => Query

  // The path to get the result of the updateMutation
  updateMutationResultPath?: string

  // Mutation to create a new instance of this type
  createMutation?: (gqlStore: StoreType, params: CreateParamType) => Query

  // The path to get the result of the createMutation
  createMutationResultPath?: string

  deleteMutation?: (gqlStore: StoreType, items: ModelType[]) => Query

  // TODO: remove, the relation model should handle it, see m2m()
  associateMutation?: (gqlStore: StoreType, loadedItem: ModelType, field: string, associatedIds: string[]) => Query

  // TODO: remove, the relation model should handle it, see m2m()
  unassociateMutation?: (gqlStore: StoreType, loadedItem: ModelType, field: string, associatedIds: string[]) => Query

  //
  // view config
  //

  title!: string

  // The name to prefix entity management screen routes. Eg. if navigationRootName is "Profile", the routes will be "ProfileList",
  // "ProfileCreate", "ProfileEdit", "ProfileDetails", "ProfileAssoc".
  navigationRootName?: string

  // The component to use for listing elements of this type. Defaults to EntityListScreen using EntityTable
  // Undefined means use default. null means ignore.
  listScreen?: (
    config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>
  ) => FC<NativeStackScreenProps<any, any>>

  // When using the default EntityListScreen, this is the configuration for that
  entityListScreenConfig: {
    title?: string | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>) => string)

    routeTitle?: string | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>) => string)

    routeName?: string | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>) => string)

    // To replace the default DataTable with own component
    tableComponent?: (config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>) => Component

    // If using default DataTable.Header add these styled props
    headerProps?:
      | StyledProps
      | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>) => StyledProps)

    // If using default DataTable.Header add these styled props
    rowComponent?: (
      config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>,
      item: ModelType
    ) => Component

    // If using default DataTable.Header add these styled props
    rowProps?:
      | StyledProps
      | ((
          config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>,
          item: ModelType
        ) => StyledProps)

    // Selection on the table. Defaults to SINGLE
    selectionType?: TableSelectionType

    // Handler when single or multuple selection on table is finalized. In case of single selection, it is fired right after onRowPress
    // In case of multiple selection onRowPress is fired after clicking any rows and onSelection is called when selection is finalized
    onSelection?:
      | null
      | ((
          event: GestureResponderEvent,
          item: ModelType | ModelType[],
          config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>
        ) => void)
      | undefined

    // Called when clicking any row of the table
    onRowPress?:
      | null
      | ((
          event: GestureResponderEvent,
          item: ModelType,
          config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>
        ) => void)
      | undefined

    // Handler when mouse enters a row
    onRowHoverIn?:
      | null
      | ((
          event: GestureResponderEvent,
          item: ModelType,
          config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>
        ) => void)
      | undefined

    // Handler when mouse exits a row
    onRowHoverOut?:
      | null
      | ((
          event: GestureResponderEvent,
          item: ModelType,
          config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>
        ) => void)
      | undefined

    //Handler when the new button pressed
    onNewPress?:
      | null
      | ((
          event: GestureResponderEvent,
          config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>
        ) => void)
      | undefined

    //Handler when the associate button pressed
    onAssocPress?:
      | null
      | ((
          event: GestureResponderEvent,
          config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>
        ) => void)
      | undefined

    // If specified a Delete button is displayed in the row, and when pressed, this function is called
    onDeletePress?:
      | null
      | ((
          event: GestureResponderEvent,
          item: ModelType,
          config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>
        ) => void)
      | undefined

    // If specified an unassociation button is displayed in the row, and when pressed, this function is called
    onUnassocPress?:
      | null
      | ((
          event: GestureResponderEvent,
          item: ModelType,
          config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>
        ) => void)
      | undefined

    // Simple filters, which can be turned on/of with a checkbox
    simpleFilters?: SimpleFilter<BoolExpType>[]

    // Can be provided to generate actions for a row in the table.
    generateActionComponent?: (
      config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>,
      item: ModelType
    ) => ReactElement
  } = {}

  // The component to use for listing elements of this type. Defaults to EntityDetailsScreen using EntityData and a number of
  // EntityTables for associated entities
  detailsScreen?: (
    config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>,
    item: ModelType
  ) => FC<NativeStackScreenProps<any, any>>

  // When using the default EntityDetailsScreen, this is the configuration for that
  entityDetailScreenConfig?: {
    title?:
      | string
      | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>, item: ModelType) => string)

    routeTitle?:
      | string
      | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>, item: ModelType) => string)

    dataTitle?:
      | string
      | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>, item: ModelType) => string)

    routeName?: string | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>) => string)
  } = {}

  dataComponent?: (
    config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>,
    item: ModelType
  ) => FC<any>

  entityDataConfig?: {
    // Custom component to generated each field of the EntityData
    dataRowGenerator?: (field: FieldConfig<ModelType>, obj: ModelType, model: any /*EntityModelType*/) => JSX.Element
    dataCellGenerator?: (field: FieldConfig<ModelType>, obj: ModelType, model: any /*EntityModelType*/) => JSX.Element
  } = {}

  assocScreen?: (
    config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>,
    item: ModelType
  ) => FC<NativeStackScreenProps<any, any>>

  // When using the default EntityDetailsScreen, this is the configuration for that
  assocScreenConfig?: {
    title?:
      | string
      | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>, item: ModelType) => string)

    routeTitle?:
      | string
      | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>, item: ModelType) => string)

    routeName?: string | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>) => string)
  } = {}

  // The component to use for creating an entity of this type. Defaults to EntityCreateScreen using EntityForm
  createFormScreen?: (
    config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>
  ) => FC<NativeStackScreenProps<any, any>>

  // When using the default EntityCreateScreen, this is the configuration for that
  entityCreateFormScreenConfig?: {
    title?: string | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>) => string)

    routeTitle?: string | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>) => string)

    routeName?: string | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>) => string)

    // Order of fields can be overriden by providing list of fields names
    fieldOrder?: string[]
    // Fields can be sectioned by defining the first field of the section and a title
    sections?: { start: string; title: string }[]
  } = {}

  // The component to use for editing an entity of this type. Defaults to EntityEditScreen using EntityForm
  editFormScreen?: (
    config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>,
    item: ModelType
  ) => FC<NativeStackScreenProps<any, any>>

  // When using the default EntityEditScreen, this is the configuration for that
  entityEditFormScreenConfig?: {
    title?:
      | string
      | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>, item: ModelType) => string)

    routeTitle?:
      | string
      | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>, item: ModelType) => string)

    routeName?: string | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>) => string)

    // Order of fields can be overriden by providing list of fields names
    fieldOrder?: string[]
    // Fields can be sectioned by defining the first field of the section and a title
    sections?: { start: string; title: string }[]
  } = {}

  // TODO: move to EntityModel
  // If onDeletePress is specified this function is called to determine whether for this specific item we should display
  // a delete button or not. You may not want to allow deletion of an item if it has some vital relationships existing.  Eg.
  // if an Organizion has members, then you don't want to let it be deleted.
  canDelete?: (
    item: ModelType,
    viewType: ViewType,
    config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>
  ) => boolean

  // If onUnassociate is specified this function is called to determine whether for this specific item we should display
  // an unassociate button or not. You may not want to allow unassociation of an item if it has some vital relationships existing.
  // In most cases canUnassociate shoudl be defined for a relation(). This one here is a global unassociation logic which is to be
  // used for any relations of the current type. In most cases we want relation specific logic.
  canUnassociate?: (
    item: ModelType,
    viewType: ViewType,
    config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>
  ) => boolean

  // TODO: move to EntityModel as action/view, move default calcFilterCond from EntityViewConfig.ts
  // Callback to provide custom Hasura filter expression based on a filter string. May use the  calcFilterCond() and buildFieldExp() in turn.
  calcFilterCond?: null | undefined | ((filter: string, stringFields: string[], numFields: string[]) => any)

  // TODO: add a helper function, which collects all fields for a model as defaults for fields so that in initial
  // implementations we don't need to configure each field manually.
  // fieldConfigForModel(SomeModel)
  fields?:
    | FieldConfig<ModelType>[]
    | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>) => FieldConfig<ModelType>[])

  // If true relations won't be generated using the relations factory below, otherwise (if false or not set) it will be.
  ignoreRelations?: boolean

  // Relations of the model. Note that this must be a function so that relations can be generated based on whether ignoreRelations is set to
  // true or not.
  relations?: () => RelationModelConfig[]

  // TODO: move to entityCreateFormScreenConfig, entityEditFormScreenConfig
  // Form configuration
  form?: {
    // Order of fields can be overriden by providing list of fields names
    fieldOrder?: string[]
    // Fields can be sectioned by defining the first field of the section and a title
    sections?: { start: string; title: string }[]
  }

  // If want to have a create button and a create form, specify this
  create?: {}

  // If want to have an edit button and an edit form, specify this
  edit?: {}

  // If want to have the ability to delete a record
  delete?: {}

  constructor(args: EntityConfigValues<ModelType, UpdateParamType, CreateParamType, BoolExpType>) {
    Object.keys(args).forEach((key) => (this[key] = args[key]))
    this._randomId = Math.random()
    if (this.navigationRootName) {
      this.navigationRootName = this.modelName
    }
  }
}

export type EntityConfigValuesAny = EntityConfigValues<any, any, any, any>

// Type without the required config parameters
export type OverridableEntityConfigValues = Partial<EntityConfigValuesAny>

export class EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType> extends EntityConfigValues<
  ModelType,
  UpdateParamType,
  CreateParamType,
  BoolExpType
> {
  // The EntityModel for which the EntityViewConfig is applied to. entityModel is untyped here to avoid
  // circular dependency. EntityModelType and EntityViewConfig can always reference each other. If you have
  // an EntityModelType then you can use the viewConfig prop, while in a EntityViewConfig the entityModel
  // prop, which needs to be casted to the actual EntityModelType which is defiend in the same field usually where
  // the EntityViewConfig is defined.
  entityModel: any

  setEntityModel(entityModel: any) {
    this.entityModel = entityModel
  }

  valueOrDefault<T, ModelType, BoolExpType>(
    val: T | ((config: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>, item: any) => T),
    def: T,
    fieldConfig: EntityConfig<ModelType, UpdateParamType, CreateParamType, BoolExpType>,
    item?: any
  ): T {
    if (val === null || val === undefined) {
      return def
    }
    if (typeof val === 'function') {
      // @ts-ignore: FieldConfig and ViewConfig don't match because of name/title, but we know we will call the
      // right type of function here
      const res = (val as (config: FieldConfig<ModelType>, any) => T)(
        // @ts-ignore
        fieldConfig,
        item
      )
      // If return value is undefined, it means we need to use the default value
      if (typeof res !== 'undefined') {
        return res
      }
      return def
    }
    return val
  }

  headerPropsVal() {
    return this.valueOrDefault(this.entityListScreenConfig?.headerProps, {}, this)
  }
  rowPropsVal(item: any) {
    return this.valueOrDefault(this.entityListScreenConfig?.rowProps, {}, this, item)
  }
  fieldsVal() {
    return this.valueOrDefault(this.fields, [], this)!
  }

  listScreenVal() {
    return this.valueOrDefault(this.listScreen, EntityListScreen, this)
  }
  listScreenTitleVal() {
    return this.valueOrDefault(this.entityListScreenConfig?.title, this.title, this)
  }
  listScreenRouteNameVal() {
    // If this is a relation use the root entity's list screen
    return this.valueOrDefault(
      this.entityListScreenConfig?.routeName,
      (this.entityModel.rootEntityModelName ? this.entityModel.rootEntityModelName : getType(this.entityModel).name) +
        'ListScreen',
      this
    )!
    //return this.valueOrDefault(this.entityListScreenConfig?.routeName, getType(this.entityModel).name+"ListScreen"+(this.entityModel.relatedFieldName?`-${this.entityModel.relatedFieldName}`:""), this)
  }
  listScreenSelectionTypeVal() {
    return this.valueOrDefault(this.entityListScreenConfig?.selectionType, TableSelectionType.SINGLE, this)
  }

  detailsScreenVal(item?: any) {
    return this.valueOrDefault(this.detailsScreen, EntityDetailsScreen, this, item)
  }
  detailsScreenTitleVal(item: any) {
    return this.valueOrDefault(
      this.entityDetailScreenConfig?.title,
      getType(this.entityModel).name + ` #${item.id}`,
      this,
      item
    )
  }
  detailsScreenDataTitleVal(item: any) {
    return this.valueOrDefault(this.entityDetailScreenConfig?.dataTitle, `Saját adatok`, this, item)
  }
  detailsScreenRouteTitleVal(item: any) {
    return this.valueOrDefault(
      this.entityDetailScreenConfig?.routeTitle,
      getType(this.entityModel).name + (item ? ` #${item.id}` : '') + ' adatai',
      this,
      item
    )
  }
  detailsScreenRouteNameVal() {
    return this.valueOrDefault(
      this.entityDetailScreenConfig?.routeName,
      (this.entityModel.rootEntityModelName ? this.entityModel.rootEntityModelName : getType(this.entityModel).name) +
        'DetailsScreen',
      this
    )!
    //return this.valueOrDefault(this.entityDetailScreenConfig?.routeName, getType(this.entityModel).name+"DetailsScreen"+(this.entityModel.relatedFieldName?`-${this.entityModel.relatedFieldName}`:""), this)
  }

  dataComponentVal(item?: any) {
    return this.valueOrDefault(this.dataComponent, EntityData, this, item)!
  }

  dataRowGeneratorVal() {
    return this.entityDataConfig?.dataRowGenerator
  }

  dataCellVal() {
    return this.entityDataConfig?.dataCellGenerator
  }

  assocScreenVal() {
    return this.valueOrDefault(this.assocScreen, EntityM2MAssocScreen, this)
  }
  assocScreenTitleVal(item: any) {
    return this.valueOrDefault(
      this.assocScreenConfig?.title,
      getType(this.entityModel).name + ` #${item.id} társítás`,
      this,
      item
    )
  }
  assocScreenRouteTitleVal(item: any) {
    return this.valueOrDefault(
      this.assocScreenConfig?.routeTitle,
      getType(this.entityModel).name + `${item ? item.id : ''} társítás`,
      this,
      item
    )
  }
  assocScreenRouteNameVal() {
    return this.valueOrDefault(
      this.assocScreenConfig?.routeName,
      (this.entityModel.rootEntityModelName ? this.entityModel.rootEntityModelName : getType(this.entityModel).name) +
        'Assoc',
      this
    )!
    //return this.valueOrDefault(this.assocScreenConfig?.routeName, getType(this.entityModel).name+"Assoc-"+(this.entityModel as any /*EntityModelType*/).relatedFieldName, this)
  }

  createFormScreenVal() {
    return this.valueOrDefault(this.createFormScreen, EntityCreateEditScreen, this)
  }
  createFormScreenTitleVal() {
    return this.valueOrDefault(this.entityCreateFormScreenConfig?.title, this.title, this)
  }
  createFormScreenRouteTitleVal() {
    return this.valueOrDefault(
      this.entityCreateFormScreenConfig?.routeTitle,
      getType(this.entityModel).name + ' felvétele',
      this
    )
  }
  createFormScreenRouteNameVal() {
    // In case of a relation edit form, use the root model's CreateScreen
    return this.valueOrDefault(
      this.entityCreateFormScreenConfig?.routeName,
      (this.entityModel.rootEntityModelName ? this.entityModel.rootEntityModelName : getType(this.entityModel).name) +
        'CreateScreen',
      this
    )!
    //return this.valueOrDefault(this.entityCreateFormScreenConfig?.routeName, getType(this.entityModel).name+"CreateScreen"+(this.entityModel.relatedFieldName?`-${this.entityModel.relatedFieldName}`:""), this)
  }

  editFormScreenVal() {
    return this.valueOrDefault(this.editFormScreen, EntityCreateEditScreen, this)
  }
  editFormScreenTitleVal(item: any) {
    return this.valueOrDefault(
      this.entityEditFormScreenConfig?.title,
      getType(this.entityModel).name + ` #${item.id}` + ' szerkesztése',
      this,
      item
    )
  }
  editFormScreenRouteTitleVal(item: any) {
    return this.valueOrDefault(
      this.entityEditFormScreenConfig?.routeTitle,
      getType(this.entityModel).name + (item ? ` #${item.id}` : '') + ' szerkesztése',
      this,
      item
    )
  }
  editFormScreenRouteNameVal() {
    // In case of a relation edit form, use the root model's EditScreen
    return this.valueOrDefault(
      this.entityEditFormScreenConfig?.routeName,
      (this.entityModel.rootEntityModelName ? this.entityModel.rootEntityModelName : getType(this.entityModel).name) +
        'EditScreen',
      this
    )!
    // THis creates a route name with -relatedFieldName appended
    //     return this.valueOrDefault(this.entityEditFormScreenConfig?.routeName, getType(this.entityModel).name+"EditScreen"+(this.entityModel.relatedFieldName?`-${this.entityModel.relatedFieldName}`:""), this)
  }

  constructor(args: EntityConfigValues<ModelType, UpdateParamType, CreateParamType, BoolExpType>) {
    super(args)
  }
}

export type EntityConfigAny = EntityConfig<any, any, any, any>

export type InputType = 'text' | 'password' | 'select' | 'date' | 'datetime' | 'assocSingle'

export type SelectValues = {
  [key: string]: string
}

export type SimpleFilter<BoolExpType> = CheckboxFilter<BoolExpType> | RadioButtonFilter<BoolExpType>

export type BaseFilter<BoolExpType> = {
  // Label to show for the checbox
  label: string

  // Whether it is checked by default
  checked?: boolean

  // Condition to apply when checked
  condition: BoolExpType

  // Assigned at runtime, don't set
  id?: string
}

export type CheckboxFilter<BoolExpType> = BaseFilter<BoolExpType> & {
  type: 'checkbox'
}

export type RadioButtonFilter<BoolExpType> = {
  type: 'radio'

  // Radio group name
  name: string

  // ARIA label
  ariaLabel: string

  // Label for the whole group
  label?: string

  // Filters with radio buttons to select. The firstswith checked=true will be the default
  // checked, or the first one if no checked=true is specified
  filters: BaseFilter<BoolExpType>[]
}

export enum TableSelectionType {
  SINGLE = 'SINGLE',
  MULTIPLE = 'MULTIPLE',
}

/**
 * Helper function to get a value of a config field, which could either be a static value or a function, which provides the
 * value. In case the value is null/undefined the def value is returned.
 * @param val
 * @param def
 * @param fieldConfig
 * @param item
 */
function valueOrDefault<T, ModelType>(
  val: T | ((config: FieldConfig<ModelType> | EntityConfigAny, item: any) => T),
  def: T,
  fieldConfig: FieldConfig<ModelType> | EntityConfigAny,
  item?: any
): T {
  if (val === null || val === undefined) {
    return def
  }
  if (typeof val === 'function') {
    // @ts-ignore: FieldConfig and ViewConfig don't match because of name/title, but we know we will call the
    // right type of function here
    const res = (val as (config: FieldConfig<ModelType>, any) => T)(
      // @ts-ignore
      fieldConfig,
      item
    )
    // If return value is undefined, it means we need to use the default value
    if (typeof res !== 'undefined') {
      return res
    }
    return def
  }
  return val
}

/**
 * Helper function to resolve actual value of config.fields, which could be a static array or a function returning the
 * array.
 * @param config
 */
// export function fieldsValue<ModelType>(config: ViewConfig<ModelType>) {
//   if (typeof config.fields === 'function') {
//     return config.fields(config);
//   }
//   return config.fields;
// }

export function emptyValues<ModelType, BoolExpType>(
  config: EntityConfig<ModelType, any, any, BoolExpType>,
  defaultEmptyValues: { [key: string]: any } | null = null
) {
  const defEmpty = defaultEmptyValues || {}
  return config.fieldsVal().reduce((emptyValues, field) => {
    if (field.editableVal() || field.editableAtCreationVal()) {
      emptyValues[field.name] = ''
    }
    return { ...emptyValues, ...defEmpty }
  }, {})
}

export function loadedValues<ModelType, BoolExpType>(
  config: EntityConfig<ModelType, any, any, BoolExpType>,
  loadedItem: ModelType
) {
  return config.fieldsVal().reduce((loadedValues, field) => {
    if (field.editableVal() || field.editableAtCreationVal() || field.viewInFormVal()) {
      // If field has a fieldValueAccessor, access item value using that
      const accessor = valueOrDefault(field.fieldValueAccessor as any, null, field)
      if (accessor) {
        loadedValues[field.name] = fieldValue(accessor, ViewType.RAW, loadedItem)
      } else {
        loadedValues[field.name] = fieldValue(field, ViewType.RAW, loadedItem)
      }
    }
    return loadedValues
  }, {})
}

export function validationSchema<ModelType, BoolExpType>(
  config: EntityConfig<ModelType, any, any, BoolExpType>,
  isCreateForm: boolean | null = null
) {
  const fieldList = config.fieldsVal()
  const cyclicFields: Array<[string, string]> = []
  const schema = Yup.object().shape(
    fieldList.reduce((shape, field) => {
      if (field.editableVal() || (field.editableAtCreationVal() && isCreateForm)) {
        // This is the default. If isCreateForm is specified, we may override it
        shape[field.name] = field.validationVal()
        const cyc = field.validationCyclicVal()
        if (cyc) {
          cyclicFields.push(cyc)
        }
        if (isCreateForm != null) {
          if (isCreateForm === true) {
            if (field.createValidation) {
              shape[field.name] = field.createValidationVal()
            }
          } else if (isCreateForm === false) {
            if (field.editValidation) {
              shape[field.name] = field.editValidationVal()
            }
          }
        }
        if (!shape[field.name]) {
          const msg = `field ${field.name} has no validation specified`
          console.error(msg)
          throw new Error(msg)
        }
      }
      return shape
    }, {}),
    cyclicFields && cyclicFields.length ? cyclicFields : undefined
  )
  console.log('++ schema', schema)
  return schema
}

/**
 * Calculates whether the input for this field should be marked as required or not.
 *
 * Note: This uses yup internal structure, so it may fails with newer yup versions.
 * @param field
 * @param validationShema
 */
export function isRequiredField<ModelType>(field: FieldConfig<ModelType>, validationShema: ObjectSchema<any>) {
  const fval = validationShema.fields[field.name]
  return fval && fval.exclusiveTests && fval.exclusiveTests.required
}

/**
 * Retunrs value of field `f` on `item`. Field maybe dotted path accessing joined value.
 * @param f
 * @param item
 */
export function fieldValue<ModelType>(
  f: FieldConfig<ModelType> | string,
  viewType: ViewType = ViewType.RAW,
  item: any
) {
  // If have a specific fieldValue function, use that
  if (typeof f === 'object' && f.fieldValue) {
    return f.fieldValue(f, viewType, item)
  }

  // Otherwise access using name or fieldValueAccessor
  const pathString =
    typeof f === 'string' ? f : f.fieldValueAccessor ? valueOrDefault(f.fieldValueAccessor as any, f.name, f) : f.name
  const path = pathString.split('.')
  if (path.length > 1) {
    // this should work both for accessing arrays if path elem is numeric and for objects if path elem is string
    let valueSoFar = item[path[0]]
    // If no value here, then it is empty, return
    if (!valueSoFar) {
      return valueSoFar
    }
    for (let i = 1; i < path.length; i++) {
      valueSoFar = valueSoFar[path[i]]
      // If no value here, then it is empty, return
      if (!valueSoFar) {
        return valueSoFar
      }
    }
    return valueSoFar
  }
  return item[pathString]
}

/**
 * Builds a Hasura query or sort expression for the field, which maybe a dotted path accessing a joined value.
 * eg: user.data.email -->
 *  user: {
 *    data: {
 *      email: exp
 *    }
 *  }
 * @param field
 * @param exp
 */
export function buildFieldExp(field: string, exp: any) {
  const path = field.split('.')
  let root: { [key: string]: any } = {}
  let leaf = root
  if (path.length > 1) {
    for (let i = 0; i < path.length - 1; i++) {
      leaf[path[i]] = {}
      leaf = leaf[path[i]]
    }
  }
  leaf[path[path.length - 1]] = exp
  return root
}

/**
 * Creates a where condition which searches for the existence of all words in filter in at leaast one of the
 * stringFields or numFields
 * @param filter filter string
 * @param stringFields fields with string values
 * @param numFields fields with numeric values
 */
export function calcFilterCond(filter: string, stringFields: string[], numFields: string[]) {
  const parts = filter.trim().split(/\s+/)

  const ands: Array<any> = []
  parts.forEach((s) =>
    ands.push({
      _or: [
        ...stringFields.map((field) => {
          return buildFieldExp(field, { _ilike: `%${s}%` })
        }),
        ...numFields
          .filter((_) => /^\d+$/.test(s))
          .map((field) => {
            return buildFieldExp(field, { _eq: s })
          }),
      ],
    })
  )

  // return final where condition
  return {
    _and: [...ands],
  }
}

/**
 * View types where field values are to be displayed. The actual field value may be different based on the current view type
 */
export enum ViewType {
  // In a listing table (EntityTable)
  LISTING = 'LISTING',

  // In a details view (EntityData)
  DETAILS = 'DETAILS',

  // In a form (EntityForm)
  FORM = 'FORM',

  // In a listing table used for associating with another entity (EntityTable)
  ASSOC_LISTING = 'ASSOC_LISTING',

  // When associating in a form (AssocSingleInput)
  ASSOC_INPUT = 'ASSOC_INPUT',

  // Default
  RAW = 'RAW',
}

export type RouteConfig = {
  // Dynamic title for the route
  title?: string

  // Parent's key in RouteConfigs
  parentRoute: string

  // Parent model from where we got to this route
  rootEntityModel?: EntityModelType

  // Parent model which lists the associations to which new items can be added
  assocEntityModel?: EntityModelType

  // Model, which should be used by this route
  entityModel?: EntityModelType

  // Used by EntityM2MAssocScreen
  relation?: RelationModelType

  // Flags whether an EntityCreateEditScreen is displayed for the root entity. In this case editing starts in the detail view so at the end
  // we will need to go back 2 levels in the navigation.
  isRootModelCreateEdit?: boolean

  // For non EntityModelType based screens a screen model needs to be passed
  screenModel?: any

  // Assoc screens
  assocField?: FieldConfig<any>

  // Callback for single association
  setAssocFieldValue?: (assocField: FieldConfig<any>, value: any) => void

  // Callback for multi association
  setAssocFieldValues?: (assocField: FieldConfig<any>, value: any[]) => void

  // Any other values specific to a screen
  [key: string]: any
}

export type RouteConfigs = {
  [key: string]: RouteConfig
}
