package gw.exportimport

uses gw.api.locale.DisplayKey
uses com.guidewire.pc.system.config.PCConfigResourceKeys
uses gw.lang.reflect.TypeSystem
uses gw.xml.XmlElement

uses java.lang.ClassNotFoundException
uses java.lang.IllegalArgumentException
uses java.lang.RuntimeException
uses java.util.ArrayList
uses java.util.Map
uses java.util.Set
uses java.util.List
uses gw.api.upgrade.Coercions

/**
 * Default EntityInfoRegistry used by ExportData. Currently sets up a map of the
 * entity CPBuilding to an EntityInfo containing all fields required for export.
 */
@ReadOnly
final class XmlEntityInfoRegistry implements EntityInfoRegistry {

  static final var INSTANCE : XmlEntityInfoRegistry as readonly Instance = new XmlEntityInfoRegistry()

  private construct () {
  }

  static final var ENTITY_INFOS : Map<Type< KeyableBean >, EntityInfo>
      = {}

  /**
   * Finds the EntityInfo associated with the bean.
   *
   * @param type A KeyableBean
   * @return EntityInfo The data associated with this entity
   */
  override reified function findEntityInfo<T extends KeyableBean>(type : Type<T>)
      : EntityInfo<T> {
    initialize()
    return ENTITY_INFOS.get(type) as EntityInfo<T>
  }

  override function getRegisteredEntityTypes() : Set<Type> {
    initialize()
    return ENTITY_INFOS.Keys
  }

  /**
   * Initialize loaded EntityInfo's if not already loaded.
   */
  function initialize() {
    if ( ENTITY_INFOS.Count == 0 ) {
      // lazy init'...
      load()
    }
  }

  /**
   * Load the configured entity flow metadata from the resources.
   *
   * Calling this will reload the metadata if it has already been loaded
   * adding any new entity flow metadata, but not removing any whose Xml
   * file has been deleted.
   */
  function load() {
    var dir = PCConfigResourceKeys.EXPORT_IMPORT_METADATA_DIR.getDir()

    dir.eachFile(\ file -> {
        var xml : XmlElement = XmlElement.parse(file)
        var entityInfo = loadFromXml(xml)
        ENTITY_INFOS.put(entityInfo.EntityType, entityInfo)
      })
  }

  /**
   * Load a configured EntityInfo from an Xml source.
   */
  function loadFromXml(xml : XmlElement) : EntityInfo {
    var entityType = loadEntityTypeFromName(
        xml.getAttributeSimpleValue("EntityTypeName").StringValue)

    if (entityType == null) {
      throw new IllegalArgumentException(
        DisplayKey.get("Import.Configuration.Errors.UnknownEntityType", "\"\""))
    }

    var parentEntityType = loadEntityTypeFromName(
        xml.getAttributeSimpleValue("ParentEntityTypeName").StringValue)
    var parentEntityColumnPath = xml.getAttributeSimpleValue("ParentEntityColumnPath").StringValue
    if (parentEntityType != null) {
      // requires ParentEntityColumnPath...
      if (! parentEntityColumnPath.NotBlank) {
        throw new IllegalArgumentException(
          DisplayKey.get("Import.Configuration.Errors.UnspecifiedParentColumnInfo", entityType))
      }
    }

    /* collect ColumnInfo Builder's from the Xml... */
    var idMap : Map<ColumnInfo.Builder, String> = {}
    var colBuilders : List<ColumnInfo.Builder> = {}
    colBuilders.addAll(loadColumnInfo(xml, idMap, entityType.Name, parentEntityColumnPath, false))
    xml.getChildren("Parent").each(\ x -> {
      colBuilders.addAll(loadColumnInfo(x, idMap, entityType.Name, parentEntityColumnPath, true))
    })

    /* validate metadata for the ColumnInfo's... */
    if ( colBuilders.Empty ) {
      throw new IllegalArgumentException(
          DisplayKey.get("Import.Configuration.Errors.MissingColumnMetadata", entityType))
    }
    if (parentEntityType != null) {
      if (! colBuilders.hasMatch(\ builder -> builder.Path == parentEntityColumnPath)) {
        throw new IllegalArgumentException(
            DisplayKey.get("Import.Configuration.Errors.MissingParentColumnInfo", entityType, parentEntityColumnPath))
      }
    }

    var cols = buildAndResolveColumnInfos(colBuilders, idMap)

    /*
     * construct the EntityInfo for the entity type using the ColumnInfo's
     * loaded from the XML...
     */
    var typedInfo = EntityInfo.Type.getParameterizedType({entityType}).TypeInfo
    var constructor = typedInfo.getConstructor({List<ColumnInfo>, Type<KeyableBean>}).Constructor
    return constructor.newInstance({cols, parentEntityType}) as EntityInfo
  }

  private function loadColumnInfo(xml : XmlElement, idMap : Map<ColumnInfo.Builder, String>, entityTypeName : String,
      parentPath : String, isParent : boolean) : List<ColumnInfo.Builder> {
    var colBuilders : List<ColumnInfo.Builder> = {}
    for ( info in xml.getChildren("ColumnInfo") ) {
      var id = info.getAttributeSimpleValue("id").StringValue
      var colBuilder = loadColumnInfoBuilderFromXml(info, entityTypeName)
      if (isParent) {
        colBuilder.parentColumn()
      }
      if (parentPath.NotBlank and parentPath == colBuilder.Path) {
        colBuilder.flagsParentEntityId()
      }
      if ( ( id != null ) and id.NotBlank ) {
        idMap[colBuilder] = id
      }
      colBuilders.add(colBuilder)
    }
    return colBuilders
  }

  /**
   * Load a configured ColumnInfo Builder from an Xml source.
   */
  private function loadColumnInfoBuilderFromXml(xml : XmlElement,
      entityTypeName : String) : ColumnInfo.Builder {
    var header = xml.getAttributeSimpleValue("Header").StringValue

    if ( gw.api.locale.DisplayKey.get(header) == null ) {
      throw new IllegalArgumentException(
          DisplayKey.get("Import.Configuration.Errors.UnknownHeaderDisplayKey", header))
    }

    var flagsAction = Coercions.makePBooleanFrom(xml.getAttributeSimpleValue("FlagsAction").GosuValue)

    var builder = new ColumnInfo.Builder()
    builder.header(header)
    var flagExcludeFromTemplate = Coercions.makePBooleanFrom(xml.getAttributeSimpleValue("ExcludeFromTemplate").GosuValue)
    if (flagExcludeFromTemplate) {
      builder.flagsExcludeFromTemplate()
    }
    if ( flagsAction ) {
      builder.flagsAction()
    } else {
      var isEntityId = Coercions.makePBooleanFrom(xml.getAttributeSimpleValue("FlagsEntityId").GosuValue)
      if ( isEntityId ) {
        builder.flagsEntityId()
      }

      builder.path(xml.getAttributeSimpleValue("Path").StringValue)

      var attrTypeName = xml.getAttributeSimpleValue("ColumnType").StringValue
      if ( attrTypeName != null ) {
        var attrType : Type
        try {
          attrType = TypeSystem.getByFullName(attrTypeName)
        } catch (ex : RuntimeException ) {
          if ( typeof(ex.Cause) == ClassNotFoundException ) {
            throw new ClassNotFoundException(
                DisplayKey.get("Import.Configuration.Errors.UnknownColumnType", entityTypeName, attrTypeName))
          }
          throw ex.Cause
        }
        builder.columnType(attrType)
      }

      if ( Coercions.makePBooleanFrom(xml.getAttributeSimpleValue("RequiredForImport").GosuValue) ) {
        builder.flagsRequiredForImport()
      }
      builder.locked(Coercions.makePBooleanFrom(xml.getAttributeSimpleValue("Locked").GosuValue) )

      var dependents = xml.getChildren("Dependent")
      if ( dependents != null and dependents.HasElements ) {
        for ( dep in dependents ) {
          var purpose = dep.getAttributeSimpleValue("purpose").StringValue
          if ( dep.$Text.NotBlank and ( purpose != null ) and purpose.NotBlank ) {
            builder.addDependentName(purpose, dep.$Text)
          }
        }
      }
    }
    return builder
  }

  /**
   * Load the named entity type, if specified.
   */
  private function loadEntityTypeFromName(entityTypeName : String) : Type {
    var entityType : Type
    if (entityTypeName != null) {
      try {
        entityType = TypeSystem.getByFullName(entityTypeName)
      } catch (ex : RuntimeException) {
        if ( typeof(ex.Cause) == ClassNotFoundException ) {
          throw new ClassNotFoundException(
            DisplayKey.get("Import.Configuration.Errors.UnknownEntityType", entityTypeName))
        }
        throw ex.Cause
      }
    }
    return entityType
  }

  /**
   * Build <code>ColumnInfo</code>'s and resolve dependencies from a
   *    <code>List</code> of <code>ColumnInfo.Builder</code>'s along with
   *    a dependency map of the builders by name.
   */
  private function buildAndResolveColumnInfos(colBuilders : List<ColumnInfo.Builder>, idMap : Map<ColumnInfo.Builder, String>) : List<ColumnInfo> {
    var cols = new ArrayList<ColumnInfo>(colBuilders.Count)
    var colsMap : Map<String, ColumnInfo> = {}
    var dependents : List<ColumnInfo.Builder> = null

    /* 1st pass builds those with no dependencies... */
    for ( var builder in colBuilders ) {
      if ( builder.hasDependentNames() ) {
        if ( dependents == null ) {
          dependents = {}
        }
        dependents.add(builder)
        cols.add(null)
      } else {
        cols.add(builder.build())
        if ( idMap.containsKey(builder) ) {
          colsMap.put(idMap[builder], cols.last())
        }
      }
    }

    if ( ( dependents != null ) and !dependents.Empty ) {
      /* more passes resolve and build those that have dependencies... */
      var resolved : int
      do {
        resolved = 0
        // use iterator to remove from list after resolving...
        var iter = dependents.iterator()
        while ( iter.hasNext() ) {
          var builder = iter.next()
          var i = colBuilders.indexOf(builder)
          if ( colsMap.Keys.containsAll(builder.DependentNames) ) {
            builder.resolveDependents(colsMap)
            cols[i] = builder.build()
            if ( idMap.containsKey(builder) ) {
              colsMap.put(idMap[builder], cols[i])
            }
            iter.remove()
            resolved += 1
          }
        }
      } while ( ( resolved != 0 ) and ! dependents.Empty )

      if ( ! dependents.Empty ) {
        throw new IllegalArgumentException(
          DisplayKey.get("Import.Configuration.Errors.UnknownDependentColumn", 
            dependents.first().DependentNames.first()))
      }
    }

    return cols
  }
}
