import { MsgParser } from '../msg-parser'
import { Tags } from '../tags'
import { FixDefinitions, MessageDefinition } from '../../dictionary/definition'
import { ISaxNode, SAXStream } from '../../dictionary'
import { ContainedField, ContainedFieldType, ContainedComponentField, ContainedGroupField, ContainedSimpleField, ContainedFieldSet } from '../../dictionary/contained'
import { SegmentDescription, SegmentType } from '../segment-description'
import { IJsFixConfig, IJsFixLogger } from '../../config'
import { MsgView } from '../msg-view'
import { Structure } from '../structure'
import { FixmlView } from './fixml-view'
import { Readable } from 'stream'

export class FiXmlParser extends MsgParser {
  private readonly locations: Tags
  private values: string[] = []
  private readonly saxStream: SAXStream
  private readonly definitions: FixDefinitions
  private readonly segments: SegmentDescription[] = []
  private readonly segmentStack: SegmentDescription[] = []
  private readonly logger: IJsFixLogger
  private last: SegmentDescription
  private raw: string
  constructor (public readonly config: IJsFixConfig,
               public readonly readStream: Readable,
               public readonly maxMessageLocations: number = 10 * 1024) {
    super()
    this.definitions = this.config.definitions
    const description = config.description
    const me = description.application.name
    this.logger = config.logFactory.logger(`${me}:FiXmlParser`)
    this.saxStream = require('sax').createStream(true, {})
    this.locations = new Tags(this.definitions, maxMessageLocations)
    this.logger.info('subscribe to stream')
    this.subscribe()
  }

  private reset (): void {
    const segments = this.segments
    while (segments.length > 0) {
      segments.pop()
    }
    const stack = this.segmentStack
    this.last = null
    this.raw = null
    this.values = []
    this.locations.reset()
    while (stack.length) {
      stack.pop()
    }
  }

  private subscribe (): void {
    const writeStream = this.saxStream
    const readStream = this.readStream
    let instance = this
    readStream.pipe(writeStream).on('ready', () => {
      this.logger.info('stream close event')
      this.emit('close')
    })
    readStream.on('error', (e) => {
      this.logger.error(e)
      this.emit('error', e)
    })
    readStream.on('end', () => {
      this.logger.info('stream end event')
      this.emit('end')
    })
    writeStream.on('data', (i: Buffer) => {
      if (instance.last) {
        instance.emit('decoded', instance.last.name, i.toString())
      } else {
        this.raw = i.toString()
      }
    })

    writeStream.on('opentag', (node) => {
      const stack = this.segmentStack
      const saxNode: ISaxNode = node as ISaxNode
      switch (saxNode.name) {
        case 'FIXML':
          this.reset()
          break

        case 'Batch': {
          const locations = this.locations
          const ptr = locations.nextTagPos - 1
          const segment: SegmentDescription = new SegmentDescription(saxNode.name,
            -1,
            null,
            ptr,
            stack.length,
            SegmentType.Batch)
          segment.startPosition = 0
          stack.push(segment)
          break
        }

        case 'Hdr': {
          this.hdr(saxNode)
          break
        }

        default: {
          const stack = this.segmentStack
          const isBatch: boolean = stack.length === 1 && stack[0].type === SegmentType.Batch
          const isMsg: boolean = stack.length === 0
          if (isMsg) {
            // one single message
            this.msg(saxNode)
          } else if (isBatch) {
            // one message within a batch or it
            this.msgInBatch(saxNode)
          } else {
            // or it is a structure within a message
            this.element(saxNode)
          }
        }
      }
    })
    writeStream.on('closetag', (name: string) => {
      this.pop(name)
    })
    writeStream.on('error', (e: Error) => {
      this.emit('error', e)
    })
  }

  private hdr (saxNode: ISaxNode): void {
    const stack = this.segmentStack
    if (stack.length === 0) {
      throw new Error(`Hdr not expected before batch or message ${saxNode.name}`)
    }
    let peek: SegmentDescription = stack[stack.length - 1]
    switch (peek.type) {
      case SegmentType.Batch: {
        // manually handle this component
        const hdr = this.definitions.component.get('StandardHeader')
        const segment: SegmentDescription = this.parseAttributes(saxNode.name, hdr, saxNode, SegmentType.Component)
        this.segmentStack.push(segment)
        break
      }

      case SegmentType.Msg: {
        this.element(saxNode)
        break
      }

      default:
        throw new Error(`Hdr not expected before batch or message ${saxNode.name}`)
    }
  }

  private msgInBatch (saxNode: ISaxNode): void {
    this.logger.info(` ${saxNode.name}: start batch`)
    const locations = this.locations
    const batch = this.segmentStack[0]
    const ptr = locations.nextTagPos
    if (!batch.delimiterPositions) {
      batch.startGroup(-1)
    }
    batch.addDelimiterPosition(ptr)
    this.logger.debug(` ${saxNode.name}: begin parse msg ${batch.delimiterPositions.length} in batch`)
    this.msg(saxNode, true)
  }

  private getView (): MsgView {
    const structure: Structure = new Structure(this.locations, this.segments)
    const last = structure.segments[structure.segments.length - 1]
    return new FixmlView(last, this.values, structure)
  }

  private pop (name: string): SegmentDescription {
    const locations = this.locations
    const stack = this.segmentStack
    const segments = this.segments
    while (stack.length > 0) {
      const pop = stack.pop()
      const ptr = locations.nextTagPos - 1
      pop.end(segments.length, ptr, locations.tagPos[ptr].tag)
      segments[segments.length] = pop
      switch (pop.type) {
        case SegmentType.Msg: {
          // raise msg event
          const last = segments[segments.length - 1]
          this.last = last
          this.emit('msg', last.name, this.getView())
          if (this.raw) {
            this.emit('decoded', this.last.name, this.raw)
            this.raw = null
          }
          break
        }
        case SegmentType.Batch: {
          const last = segments[segments.length - 1]
          this.logger.debug(`emit batch with ${pop.delimiterPositions.length} elements`)
          this.emit('batch', last.set.abbreviation, this.getView())
          break
        }
      }
      if (pop.name === name) {
        return pop
      }
    }
    return null
  }

  private startGroup (saxNode: ISaxNode, gf: ContainedGroupField) {
    const locations = this.locations
    const stack: SegmentDescription[] = this.segmentStack
    const ptr = locations.nextTagPos
    const def = gf.definition
    const segment: SegmentDescription = this.parseAttributes(saxNode.name, def, saxNode, SegmentType.Component)
    const group: SegmentDescription = new SegmentDescription(def.name,
      locations.tagPos[ptr].tag,
      def,
      ptr,
      stack.length,
      SegmentType.Group)
    group.startGroup(locations.tagPos[ptr].tag)
    group.addDelimiterPosition(ptr)
    stack.push(group)
    stack.push(segment)
  }

  private getNextField (saxNode: ISaxNode): ContainedField {
    const stack: SegmentDescription[] = this.segmentStack
    while (stack.length > 0) {
      let peek: SegmentDescription = stack[stack.length - 1]
      let field = peek.set.localNameToField.get(saxNode.name)
      if (field) {
        return field
      }
      // if this is a group of the same type as already on stack
      // take the field from the next level up
      if (peek.type === SegmentType.Group && stack.length > 1) {
        const contained = stack[stack.length - 2].set.localNameToField.get(saxNode.name)
        if (contained instanceof ContainedGroupField) {
          if (contained.definition.name === peek.name) {
            // this is the same type for next instance in the same group.
            return contained
          }
        }
      }
      // then have ended previous group. Pop and return to see if this field lives on the parent
      const locations = this.locations
      const ptr = locations.nextTagPos - 1
      const pop = stack.pop()
      const segments = this.segments
      pop.end(segments.length, ptr, locations.tagPos[ptr].tag)
      segments[segments.length] = pop
    }
    return null
  }

  private dispatch (saxNode: ISaxNode, field: ContainedField) {
    switch (field.type) {
      case ContainedFieldType.Component: {
        const cf: ContainedComponentField = field as ContainedComponentField
        const segment: SegmentDescription = this.parseAttributes(saxNode.name, cf.definition, saxNode, SegmentType.Component)
        this.segmentStack.push(segment)
        break
      }

      case ContainedFieldType.Group: {
        this.dispatchGroup(saxNode, field as ContainedGroupField)
        break
      }
    }
  }

  private dispatchGroup (saxNode: ISaxNode, gf: ContainedGroupField): void {
    const stack: SegmentDescription[] = this.segmentStack
    const peek: SegmentDescription = stack[stack.length - 1]
    switch (peek.type) {
      case SegmentType.Msg:
      case SegmentType.Component: {
        this.startGroup(saxNode, gf)
        break
      }

      case SegmentType.Group: {
        if (gf.name === saxNode.name) {
          const ptr = this.locations.nextTagPos
          peek.addDelimiterPosition(ptr)
          stack[stack.length] = this.parseAttributes(saxNode.name, gf.definition, saxNode, SegmentType.Component)
        } else {
          throw new Error(`expected another group instance of ${gf.name} but got ${saxNode.name}`)
        }
        break
      }

      default:
        throw new Error(`dispatchGroup has field ${gf.name} peek type ${peek.type}`)
    }
  }

  private element (saxNode: ISaxNode): void {
    // may terminate a group and move fields
    const field = this.getNextField(saxNode)
    if (!field) {
      const stack: SegmentDescription[] = this.segmentStack
      let peek: SegmentDescription = stack[stack.length - 1]
      throw new Error(`field ${saxNode.name} not known in set ${peek.set.name}`)
    }
    this.dispatch(saxNode, field)
  }

  private msg (saxNode: ISaxNode, inBatch: boolean = false): void {
    this.logger.debug(`${saxNode.name}: begin parse msg`)
    const type: string = saxNode.name
    const def: MessageDefinition = this.definitions.message.get(type)
    if (!def) {
      throw new Error(`unknown message type ${type}`)
    }
    if (inBatch) {
      const batch = this.segmentStack[0]
      batch.set = def
    }
    const segment: SegmentDescription = this.parseAttributes(type, def, saxNode, SegmentType.Msg)
    this.segmentStack.push(segment)
  }

  private parseAttributes (name: string, set: ContainedFieldSet, saxNode: ISaxNode, type: SegmentType): SegmentDescription {
    const locations = this.locations
    const attributes = saxNode.attributes
    const values = this.values
    let ptr: number
    if (attributes) {
      const keys: string[] = Object.keys(attributes)
      ptr = keys.length > 0 ? locations.nextTagPos : locations.nextTagPos - 1
      for (let j: number = 0; j < keys.length; ++j) {
        const k: string = keys[j]
        const v: string = attributes[k]
        const field: ContainedSimpleField = set.localNameToField.get(k) as ContainedSimpleField
        if (!field) {
          this.logger.warning(`no field ${k} in set ${set.name}`)
          locations.store(j, 1, -1)
        } else {
          locations.store(j, 1, field.definition.tag)
        }
        values[values.length] = v
      }
      return new SegmentDescription(name, locations.tagPos[ptr].tag, set, ptr, this.segmentStack.length, type)
    }
  }
}
