// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { ParseError } from './ParseError';
import { type Tokenizer, type Token, TokenKind } from './Tokenizer';
import { type AstNode, AstScript, AstCommand, AstCompoundWord, AstText } from './AstNode';

export class Parser {
  private readonly _tokenizer: Tokenizer;
  private _peekedToken: Token | undefined;

  public constructor(tokenizer: Tokenizer) {
    this._tokenizer = tokenizer;
    this._peekedToken = undefined;
  }

  public parse(): AstScript {
    const script: AstScript = new AstScript();

    const startingToken: Token = this._peekToken();

    const astCommand: AstCommand | undefined = this._parseCommand();

    if (!astCommand) {
      throw new ParseError('Expecting a command', startingToken.range);
    }

    const nextToken: Token = this._peekToken();

    if (nextToken.kind !== TokenKind.EndOfInput) {
      throw new ParseError(`Unexpected token: ${TokenKind[nextToken.kind]}`, nextToken.range);
    }

    script.body = astCommand;

    return script;
  }

  private _parseCommand(): AstCommand | undefined {
    this._skipWhitespace();

    const startingToken: Token = this._peekToken();

    const command: AstCommand = new AstCommand();
    command.commandPath = this._parseCompoundWord();
    if (!command.commandPath) {
      throw new ParseError('Expecting a command path', startingToken.range);
    }

    while (this._skipWhitespace()) {
      const compoundWord: AstCompoundWord | undefined = this._parseCompoundWord();
      if (!compoundWord) {
        break;
      }
      command.arguments.push(compoundWord);
    }

    return command;
  }

  private _parseCompoundWord(): AstCompoundWord | undefined {
    const compoundWord: AstCompoundWord = new AstCompoundWord();

    for (;;) {
      const node: AstNode | undefined = this._parseText();
      if (!node) {
        break;
      }
      compoundWord.parts.push(node);
    }

    if (compoundWord.parts.length === 0) {
      // We didn't parse a word
      return undefined;
    }

    return compoundWord;
  }

  private _parseText(): AstText | undefined {
    const token: Token = this._peekToken();

    if (token.kind === TokenKind.Text) {
      this._readToken();

      const astText: AstText = new AstText();
      astText.token = token;
      astText.range = token.range;
      return astText;
    }

    return undefined;
  }

  /**
   * Skips any whitespace tokens.  Returns true if any whitespace was actually encountered.
   */
  private _skipWhitespace(): boolean {
    let sawWhitespace: boolean = false;
    while (this._peekToken().kind === TokenKind.Spaces) {
      this._readToken();
      sawWhitespace = true;
    }
    if (this._peekToken().kind === TokenKind.EndOfInput) {
      sawWhitespace = true;
    }
    return sawWhitespace;
  }

  private _readToken(): Token {
    if (this._peekedToken) {
      const token: Token = this._peekedToken;
      this._peekedToken = undefined;
      return token;
    } else {
      return this._tokenizer.readToken();
    }
  }

  private _peekToken(): Token {
    if (!this._peekedToken) {
      this._peekedToken = this._tokenizer.readToken();
    }
    return this._peekedToken;
  }
}
