//
// Copyright (c) 2015, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   25 Apr 15  Matthew Giannini  Creation
//

**
** Models an HTTP challenge/response authentication scheme as defined
** in [RFC7235]`https://tools.ietf.org/html/rfc7235`. Auth schemes have
** a case-insensitive name, and either
**   1. a single value, or
**   1. a a map of auth parameters that have case-insensitive keys.
**
@Js const class WebAuthScheme
{
  ** Make an auth scheme with the given name and a map of auth-params.
  new makeParams(Str name, Str:Str params := [:])
  {
    this.name = name
    if (!params.caseInsensitive)
    {
      m := [Str:Str][:] { caseInsensitive = true }
      params.each |v, k| { m[k] = v }
      params = m
    }
    this.params = params.toImmutable
  }

  ** Make an auth scheme with the given name and token68 value.
  new makeToken68(Str name, Str tok68)
  {
    this.name  = name
    if (!AuthParser.isToken68(tok68)) throw ArgErr("Not a token68: '${tok68}'")
    this.tok68 = tok68
  }

  WebAuthScheme addParams(Str:Str params)
  {
    WebAuthScheme(name, this.params.dup.addAll(params))
  }

  ** Case-insensitive check to see if scheme matches name
  Bool isScheme(Str name)
  {
    this.name.lower == name.lower
  }

  ** True if the auth scheme is using the 'token68' syntax.
  Bool isToken68() { tok68 != null}

  ** Get a value from the auth-params, or return the 'defVal'.
  @Operator
  Str? get(Str param, Str? defVal := null)
  {
    params[param] ?: defVal
  }

  ** Encode the auth scheme for use as a header value.
  override Str toStr()
  {
    buf := StrBuf()
    buf.add(name)
    if (isToken68) buf.add(" ${tok68}")
    else if (!params.isEmpty) buf.add(" ${encodeParams(params)}")
    return buf.toStr
  }

  @NoDoc static Str encodeParams(Str:Str params)
  {
    buf := StrBuf()
    i := 0
    params.each |v, k| {
      if (i > 0) buf.add(", ")
      buf.add(k).add("=").add(WebUtil.toQuotedStr(v))
      ++i
    }
    return buf.toStr
  }

  ** The auth scheme name
  const Str name

  ** The auth params for this scheme.
  const Str:Str params := [:]

  ** The token68 value of this scheme.
  const Str? tok68 := null
}

**
** AuthParser
**
@NoDoc @Js internal class AuthParser
{
  new make(Str val)
  {
    this.buf = val
    reset(0)
  }

  Str:Str authParams()
  {
    params := parseAuthParams
    if (!eof) throw ParseErr("Invalid auth param list: ${buf}")
    return params
  }

  WebAuthScheme? nextScheme()
  {
    if (eof) return null
    if (pos > 0) commaOws

    name := parseToken([SP, COMMA, EOF])
    if (cur != SP) return WebAuthScheme(name)

    while (cur == SP) consume

    start  := pos
    tok68  := parseToken68([COMMA, EOF])
    if (tok68 != null) return WebAuthScheme(name, tok68)

    reset(start)
    params := parseAuthParams
    if (params.isEmpty) throw ParseErr("Expected token68 or #auth-param at pos ${pos}: '${buf}'")
    return WebAuthScheme(name, params)
  }

//////////////////////////////////////////////////////////////////////////
// Parsing
//////////////////////////////////////////////////////////////////////////

  ** Parse auth params until we don't find any more. Does not necessarily
  ** consume the entire buf.
  private Str:Str parseAuthParams()
  {
    params := Str:Str[:] { caseInsensitive = true }
    while (true)
    {
      start := pos
      if (eof) break
      if (params.size > 0) commaOws
      if (!parseAuthParam(params)) { reset(start); break }
      ows
    }
    return params
  }

  ** Parse a single auth param.
  private Bool parseAuthParam(Str:Str params)
  {
    if (eof) return false

    start := pos
    key := parseToken([SP, HTAB, EQ, COMMA, EOF])
    ows
    if (cur != EQ)
    {
      // backtrack
      reset(start)
      return false
    }
    consume
    ows
    val := cur == DQUOT ? parseQuotedString : parseToken([SP, HTAB, COMMA, EOF])
    ows
    params[key] = val
    return true
  }

  private Str? parseToken68(Int[] terms)
  {
    tok := parseUntil(terms)
    return isToken68(tok) ? tok : null
  }

  static Bool isToken68(Str s)
  {
    if (s.isEmpty) return false
    eq := false
    return s.all |Int c, Int i->Bool| {
      // after first '=', everything must be '='
      if (c == EQ && i > 0) eq = true
      if (eq) return c == EQ
      return c.isAlpha || c.isDigit || Tok68Special.containsChar(c)
    }
  }
  private static const Str Tok68Special := "-._~+/"

  private Str parseToken(Int[] terms)
  {
    verifyToken(parseUntil(terms))
  }

  private Str parseUntil(Int[] terms)
  {
    start := pos
    while(true)
    {
      if (eof)
      {
        if (terms.contains(EOF)) break
        throw ParseErr("Unexpected <eof>: $buf")
      }
      if (terms.contains(cur)) break
      consume
    }
    return buf[start..<pos]
  }

  private Void ows()
  {
    while (isOWS) consume
  }

  private Bool isOWS()
  {
    cur == SP || cur == HTAB
  }

  private Str parseQuotedString()
  {
    start := pos
    if (cur != DQUOT) throw ParseErr("Expected '$DQUOT' at pos ${pos}")
    consume
    while (true)
    {
      if (eof) throw ParseErr("Unterminated quoted-string starting at ${pos}")
      if (cur == DQUOT) { consume; break }
      if (cur == ESC && peek == DQUOT) { consume; consume }
      else consume
    }
    return WebUtil.fromQuotedStr(buf[start..<pos])
  }

  private Str verifyToken(Str tok)
  {
    if (!WebUtil.isToken(tok)) throw ParseErr("Expected token, not '$tok'")
    return tok
  }

  private Void commaOws()
  {
    if (cur != COMMA) throw ParseErr("Expected ',': ${buf[0..pos]}")
    consume
    ows
  }

  Bool eof() { cur == EOF }

//////////////////////////////////////////////////////////////////////////
// Consume
//////////////////////////////////////////////////////////////////////////

  private Void consume()
  {
    cur = peek
    ++pos
    if (pos+1 < buf.size)
      peek = buf[pos+1]
    else
      peek = EOF
  }

  private Void reset(Int pos)
  {
    this.pos = pos - 2
    consume
    consume
  }

//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////

  private const static Int SP    := ' '
  private const static Int HTAB  := '\t'
  private const static Int EQ    := '='
  private const static Int COMMA := ','
  private const static Int DQUOT := '"'
  private const static Int ESC   := '\\'
  private const static Int EOF   := -1

  private const Str buf
  private Int pos  := -2
  private Int cur  := EOF
  private Int peek := EOF
}