import {type, typeMatcher} from './type'

export class TypeParser {
  validDataTypes = ['String', 'Array', 'SimpleObject', 'ComplexObject', 'Primitive']

  constructor (data) {
    this.data = data
  }

  // ==========================================================================
  // Validation
  // ==========================================================================

  validateDataType () {
    const dataType = this.data.dataType
    if (dataType && !this.validDataTypes.includes(dataType)) {
      throw new RangeError(`dataType was ${dataType}; expected one of ${this.validDataTypes}`)
    }
  }

  validateParseType () {
    const predicate = this.data.predicate
    if (predicate && !(predicate instanceof RegExp || typeof predicate === 'function')) {
      throw new TypeError(`predicate was ${typeof predicate}; expected RegExp or function`)
    }
  }

  validatePropertyConstraint () {
    const propertyConstraint = this.data.propertyConstraint
    if (propertyConstraint && typeof propertyConstraint !== 'object') {
      throw new TypeError(`propertyConstraint was ${typeof propertyConstraint}; expected array or object`)
    }
  }

  validateElementConstraint () {
    const elementConstraint = this.data.elementConstraint
    if (elementConstraint && typeof elementConstraint !== 'string') {
      throw new TypeError(`elementConstraint was ${typeof elementConstraint}; expected string`)
    }
  }

  validateExtends () {
    const extend = this.data.extends
    if (extend && typeof extend !== 'string') {
      throw new TypeError(`extends was ${typeof extend}; expected string`)
    }
  }

  validate () {
    if (this.data !== null && typeof this.data !== 'object') {
      throw new TypeError(`typeParser was ${typeof this.data}; expected object`)
    }
    this.validateDataType()
    this.validateParseType()
    this.validatePropertyConstraint()
    this.validateElementConstraint()
    this.validateExtends()
  }

  // ==========================================================================
  // Simplification helpers
  // ==========================================================================

  parsePropertyConstraint () {
    let constraints = [].concat(this.data.propertyConstraint || [])

    return constraints.map(({props, match = 'every', value = () => true}) => {
      props = [].concat(props)

      return input => props[match](prop => prop in input && value(input[prop]))
    })
  }

  parseElementConstraint () {
    let constraint = this.data.elementConstraint
    return !constraint ? [] : [input => input.every(entry => type(entry) === constraint)]
  }

  parsePredicate () {
    if (this.data.predicate instanceof RegExp) {
      return [this.data.predicate.test.bind(this.data.predicate)]
    } else if (this.data.predicate) {
      return [this.data.predicate]
    } else {
      return []
    }
  }

  getCombinedPredicate () {
    let predicates = [
      ...this.parsePredicate(),
      ...this.parsePropertyConstraint(),
      ...this.parseElementConstraint()
    ]

    if (predicates.length === 0) {
      return () => true
    } else if (predicates.length === 1) {
      return predicates[0]
    } else {
      return input => predicates.every(predicate => predicate(input))
    }
  }

  getDataType () {
    if (this.data.dataType) {
      return this.data.dataType
    } else if (this.data.predicate instanceof RegExp) {
      return 'String'
    } else if (this.data.elementConstraint) {
      return 'Array'
    } else {
      return 'Primitive'
    }
  }

  // ==========================================================================
  // Data simplification
  // ==========================================================================

  get dataType () {
    return this.getDataType()
  }

  get predicate () {
    return this.getCombinedPredicate()
  }

  get extends () {
    return this.data.extends
  }
}

export class DataParser {
  constructor (parser, {async} = {}) {
    this.parser = parser
    this.async = async
  }

  // ==========================================================================
  // Validation
  // ==========================================================================

  validate () {
    const parser = this.parser
    if (typeof parser !== 'function') {
      throw new TypeError(`parser was ${typeof parser}; expected function`)
    }
  }
}

export class FormatParser {
  constructor (format, parsers = {}) {
    this.format = format

    if (parsers.parseType) {
      this.typeParser = new TypeParser(parsers.parseType)
    }
    if (parsers.parse) {
      this.dataParser = new DataParser(parsers.parse, {async: false})
    }
    if (parsers.parseAsync) {
      this.asyncDataParser = new DataParser(parsers.parseAsync, {async: true})
    }
  }

  // ==========================================================================
  // Validation
  // ==========================================================================

  validateFormat () {
    const format = this.format
    if (!typeMatcher.test(format)) {
      throw new TypeError(`format name was "${format}"; didn't match expected pattern`)
    }
  }

  validate () {
    this.validateFormat()
    if (this.typeParser) {
      this.typeParser.validate()
    }
    if (this.dataParser) {
      this.dataParser.validate()
    }
    if (this.asyncDataParser) {
      this.asyncDataParser.validate()
    }
  }
}

@@ -858,32 +858,254 @@
 on%60)%0A    %7D%0A  %7D%0A%0A
+  validateTokenList () %7B%0A    const tokenList = this.data.tokenList%0A    if (tokenList && typeof tokenList !== 'object') %7B%0A      throw new TypeError(%60tokenList was $%7Btypeof tokenList%7D; expected object or RegExp%60)%0A    %7D%0A  %7D%0A%0A
   validateProper
@@ -1114,32 +1114,32 @@
 Constraint () %7B%0A
-
     const proper
@@ -2053,24 +2053,53 @@
 ParseType()%0A
+    this.validateTokenList()%0A
     this.val
@@ -2382,16 +2382,518 @@
 ======%0A%0A
+  parseTokenList () %7B%0A    let tokenList = this.data.tokenList%0A%0A    if (!tokenList) %7B%0A      return %5B%5D%0A    %7D else if (tokenList instanceof RegExp) %7B%0A      tokenList = %7Btoken: tokenList%7D%0A    %7D%0A%0A    let %7Btoken, split = /%5Cs+/, trim = true, every = true%7D = tokenList%0A%0A    let trimInput = (input) =%3E trim ? input.trim() : input%0A    let testTokens = every ? 'every' : 'some'%0A%0A    let predicate = (input) =%3E%0A      trimInput(input).split(split)%5BtestTokens%5D(part =%3E token.test(part))%0A%0A    return %5Bpredicate%5D%0A  %7D%0A%0A
   parseP
@@ -3690,24 +3690,56 @@
 redicate(),%0A
+      ...this.parseTokenList(),%0A
       ...thi
@@ -4112,24 +4112,24 @@
 ta.dataType%0A
-
     %7D else i
@@ -4194,16 +4194,76 @@
 String'%0A
+    %7D else if (this.data.tokenList) %7B%0A      return 'String'%0A
     %7D el

Add typeParser option tokenList