/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */
import { MemoryValue, Memory } from './Memory'
import { ModelUtils } from './ModelUtils'
import { EntityBase } from './Entity'

export interface FilledEntity {
  entityId: string | null
  values: MemoryValue[]
}

export const filledEntityValueAsString = (fe: FilledEntity): string => memoryValuesAsString(fe.values)

export const memoryValuesAsString = (memories: MemoryValue[]): string => {
  // Print out list in friendly manner
  let group = ''
  for (let key in memories) {
    let index = +key
    let prefix = ''
    if (memories.length !== 1 && index === memories.length - 1) {
      prefix = ' and '
    } else if (index !== 0) {
      prefix = ', '
    }
    let value = memories[key]
    let text = value.displayText ? value.displayText : value.userText
    group += `${prefix}${text}`
  }
  return group
}

// In future will refactor to remove FilledEntityMap, but this method provides abstraction
// or isolation to methods that would normally consume filledEntityMap to reduce coupling
// and make code more flexible
export const getEntityDisplayValueMap = (filledEntityMap: FilledEntityMap): Map<string, string> => {
  return Object.keys(filledEntityMap.map).reduce((m, key) => {
    const entityDisplayValue = filledEntityMap.ValueAsString(key)

    if (entityDisplayValue) {
      m.set(key, entityDisplayValue)
    }

    // Entity is missing. Provide readable error message
    else {
      // Default to showing key
      let missingValue = key

      // If key is an entityId, try to get value from name key
      if (filledEntityMap.map[key].entityId === key) {
        for (let k of Object.keys(filledEntityMap.map)) {
          if (k !== key && filledEntityMap.map[k].entityId === key) {
            missingValue = k
          }
        }
      }
      m.set(key, `[$${missingValue}]`)
    }

    return m
  }, new Map<string, string>())
}

export class FilledEntityMap {
  public map: { [key: string]: FilledEntity } = {}

  public constructor(init?: Partial<FilledEntityMap>) {
    Object.assign(this, init)
  }

  public static FromFilledEntities(filledEntities: FilledEntity[], entities: EntityBase[]): FilledEntityMap {
    let filledEntityMap = new FilledEntityMap()
    for (let filledEntity of filledEntities) {
      let entity = entities.find(e => e.entityId === filledEntity.entityId)
      if (entity) {
        filledEntityMap.map[entity.entityName] = filledEntity
      }
    }
    return filledEntityMap
  }

  // Flips entity name based map to entityId based map
  public EntityMapToIdMap() {
    let filledEntityMap = new FilledEntityMap()
    for (let entityName in this.map) {
      const filledEntity = this.map[entityName]
      filledEntityMap.map[filledEntity.entityId!] = filledEntity
    }
    return filledEntityMap
  }

  // Provided an update (diff), update the filled entity list
  public UpdateFilledEntities(filledEntities: FilledEntity[], entities: EntityBase[]) {
    for (let filledEntity of filledEntities) {
      let entity = entities.find(e => e.entityId === filledEntity.entityId)
      if (entity) {
        this.map[entity.entityName] = filledEntity
      }
    }
  }

  public ToMemory(): Memory[] {
    let memory: Memory[] = []
    for (let entityName in this.map) {
      let entityValues = this.map[entityName] ? this.map[entityName].values : []
      memory.push({ entityName: entityName, entityValues })
    }
    return memory
  }

  public ValueAsList(entityName: string): string[] {
    if (!this.map[entityName]) {
      return []
    }

    return this.map[entityName].values.filter(v => typeof v.userText === 'string').map(v => v.userText!)
  }

  public ValueAsString(entityName: string): string | null {
    if (!this.map[entityName]) {
      return null
    }

    if (this.map[entityName].values.length === 0) {
      return null
    }
    // Print out[] list in friendly manner
    return filledEntityValueAsString(this.map[entityName])
  }

  public ValueAsNumber(entityName: string): number | null {
    const textObj = this.ValueAsString(entityName)
    let number = Number(textObj)
    if (isNaN(number)) {
      return null
    }
    return number
  }

  public ValueAsBoolean(entityName: string): boolean | null {
    const textObj = this.ValueAsString(entityName)
    if (textObj) {
      if (textObj.toLowerCase() === 'true') {
        return true
      }
      if (textObj.toLowerCase() === 'false') {
        return false
      }
    }
    return null
  }

  public ValueAsObject<T>(entityName: string): T | null {
    const textObj = this.ValueAsString(entityName)
    if (textObj) {
      return JSON.parse(textObj) as T
    }
    return null
  }

  public Values(entityName: string): MemoryValue[] {
    if (!this.map[entityName]) {
      return []
    }
    return this.map[entityName].values
  }

  public Forget(entityName: string, entityValue: string | null = null, isBucket: boolean = false): void {
    // Check if entity buckets values
    if (isBucket) {
      // Entity might not be in memory
      if (!this.map[entityName]) {
        return
      }

      // If no entity Value provide, clear the entity
      if (!entityValue) {
        delete this.map[entityName]
      } else {
        // Find case insensitive index
        let lowerCaseNames = this.map[entityName].values.filter(mv => mv.userText).map(mv => mv.userText!.toLowerCase())

        let index = lowerCaseNames.indexOf(entityValue.toLowerCase())
        if (index > -1) {
          this.map[entityName].values.splice(index, 1)
          if (this.map[entityName].values.length === 0) {
            delete this.map[entityName]
          }
        }
      }
    } else {
      delete this.map[entityName]
    }
  }

  // Remember multiple values for an entity
  public RememberMany(
    entityName: string,
    entityId: string,
    entityValues: string[],
    isBucket: boolean = false,
    builtinType: string | null = null,
    resolution: {} | null = null
  ): void {

    // Filter out empty strings
    const values = entityValues.filter(s => s !== "")

    // If no non-empty values, acts as delete
    if (values.length === 0) {
      this.Forget(entityName)
      return
    }

    for (let entityValue of values) {
      this.Remember(entityName, entityId, entityValue, isBucket, builtinType, resolution)
    }
  }

  // Remember value for an entity
  public Remember(
    entityName: string,
    entityId: string,
    entityValue: string,
    isBucket: boolean = false,
    builtinType: string | null = null,
    resolution: any | null = null,
    enumValueId: string | undefined = undefined
  ): void {

    // If setting to an empty string is actually a delete
    if (entityValue === "") {
      this.Forget(entityName)
      return
    }

    // If we don't already have entry in map for this item, create one
    if (!this.map[entityName]) {
      this.map[entityName] = {
        entityId,
        values: []
      }
    }

    const displayText = builtinType && resolution ? ModelUtils.PrebuiltDisplayText(builtinType, resolution, entityValue) : entityValue

    const newFilledEntityValue: MemoryValue = {
      userText: entityValue,
      displayText,
      builtinType,
      resolution,
      enumValueId
    }

    const filledEntity = this.map[entityName]
    // Check if entity buckets values
    if (isBucket) {
      // Add if not a duplicate
      const containsDuplicateValue = filledEntity.values.some(memoryValue => memoryValue.userText === entityValue)
      if (!containsDuplicateValue) {
        filledEntity.values.push(newFilledEntityValue)
      }
    } else {
      filledEntity.values = [newFilledEntityValue]
    }
  }

  /** Return FilledEntity array for items I've remembered */
  public FilledEntities(): FilledEntity[] {
    return Object.values(this.map)
  }

  public static Split(action: string): string[] {
    return action.split(/[\s,:.?!\[\]]+/)
  }
}
