import { ValueSpec, DefaultString } from './config-types'

export class Range {
  min?: number
  max?: number
  minInclusive: boolean
  maxInclusive: boolean

  static from(s: string): Range {
    const r = new Range()
    r.minInclusive = s.startsWith('[')
    r.maxInclusive = s.endsWith(']')
    const [minStr, maxStr] = s.split(',').map(a => a.trim())
    r.min = minStr === '(*' ? undefined : Number(minStr.slice(1))
    r.max = maxStr === '*)' ? undefined : Number(maxStr.slice(0, -1))
    return r
  }

  checkIncludes(n: number) {
    if (
      this.hasMin() !== undefined &&
      (this.min > n || (!this.minInclusive && this.min == n))
    ) {
      throw new Error(this.minMessage())
    }
    if (
      this.hasMax() &&
      (this.max < n || (!this.maxInclusive && this.max == n))
    ) {
      throw new Error(this.maxMessage())
    }
  }

  hasMin(): boolean {
    return this.min !== undefined
  }

  hasMax(): boolean {
    return this.max !== undefined
  }

  minMessage(): string {
    return `greater than${this.minInclusive ? ' or equal to' : ''} ${this.min}`
  }

  maxMessage(): string {
    return `less than${this.maxInclusive ? ' or equal to' : ''} ${this.max}`
  }

  description(): string {
    let message = 'Value can be any number.'

    if (this.hasMin() || this.hasMax()) {
      message = 'Value must be'
    }

    if (this.hasMin() && this.hasMax()) {
      message = `${message} ${this.minMessage()} AND ${this.maxMessage()}.`
    } else if (this.hasMin() && !this.hasMax()) {
      message = `${message} ${this.minMessage()}.`
    } else if (!this.hasMin() && this.hasMax()) {
      message = `${message} ${this.maxMessage()}.`
    }

    return message
  }

  integralMin(): number | undefined {
    if (this.min) {
      const ceil = Math.ceil(this.min)
      if (this.minInclusive) {
        return ceil
      } else {
        if (ceil === this.min) {
          return ceil + 1
        } else {
          return ceil
        }
      }
    }
  }

  integralMax(): number | undefined {
    if (this.max) {
      const floor = Math.floor(this.max)
      if (this.maxInclusive) {
        return floor
      } else {
        if (floor === this.max) {
          return floor - 1
        } else {
          return floor
        }
      }
    }
  }
}

export function getDefaultDescription(spec: ValueSpec): string {
  let toReturn: string | undefined
  switch (spec.type) {
    case 'string':
      if (typeof spec.default === 'string') {
        toReturn = spec.default
      } else if (typeof spec.default === 'object') {
        toReturn = 'random'
      }
      break
    case 'number':
      if (typeof spec.default === 'number') {
        toReturn = String(spec.default)
      }
      break
    case 'boolean':
      toReturn = spec.default === true ? 'True' : 'False'
      break
    case 'enum':
      toReturn = spec['value-names'][spec.default]
      break
  }

  return toReturn || ''
}

export function getDefaultString(defaultSpec: DefaultString): string {
  if (typeof defaultSpec === 'string') {
    return defaultSpec
  } else {
    let s = ''
    for (let i = 0; i < defaultSpec.len; i++) {
      s = s + getRandomCharInSet(defaultSpec.charset)
    }

    return s
  }
}

// a,g,h,A-Z,,,,-
export function getRandomCharInSet(charset: string): string {
  const set = stringToCharSet(charset)
  let charIdx = Math.floor(Math.random() * set.len)
  for (let range of set.ranges) {
    if (range.len > charIdx) {
      return String.fromCharCode(range.start.charCodeAt(0) + charIdx)
    }
    charIdx -= range.len
  }
  throw new Error('unreachable')
}

function stringToCharSet(charset: string): CharSet {
  let set: CharSet = { ranges: [], len: 0 }
  let start: string | null = null
  let end: string | null = null
  let in_range = false
  for (let char of charset) {
    switch (char) {
      case ',':
        if (start !== null && end !== null) {
          if (start!.charCodeAt(0) > end!.charCodeAt(0)) {
            throw new Error('start > end of charset')
          }
          const len = end.charCodeAt(0) - start.charCodeAt(0) + 1
          set.ranges.push({
            start,
            end,
            len,
          })
          set.len += len
          start = null
          end = null
          in_range = false
        } else if (start !== null && !in_range) {
          set.len += 1
          set.ranges.push({ start, end: start, len: 1 })
          start = null
        } else if (start !== null && in_range) {
          end = ','
        } else if (start === null && end === null && !in_range) {
          start = ','
        } else {
          throw new Error('unexpected ","')
        }
        break
      case '-':
        if (start === null) {
          start = '-'
        } else if (!in_range) {
          in_range = true
        } else if (in_range && end === null) {
          end = '-'
        } else {
          throw new Error('unexpected "-"')
        }
        break
      default:
        if (start === null) {
          start = char
        } else if (in_range && end === null) {
          end = char
        } else {
          throw new Error(`unexpected "${char}"`)
        }
    }
  }
  if (start !== null && end !== null) {
    if (start!.charCodeAt(0) > end!.charCodeAt(0)) {
      throw new Error('start > end of charset')
    }
    const len = end.charCodeAt(0) - start.charCodeAt(0) + 1
    set.ranges.push({
      start,
      end,
      len,
    })
    set.len += len
  } else if (start !== null) {
    set.len += 1
    set.ranges.push({
      start,
      end: start,
      len: 1,
    })
  }
  return set
}

interface CharSet {
  ranges: {
    start: string
    end: string
    len: number
  }[]
  len: number
}
