///###////////////////////////////////////////////////////////////////////////
//
// Burton Computer Corporation
// http://www.burton-computer.com
// http://www.cooldevtools.com
// $Id: HdlSyntaxChecker.cc 272 2007-01-06 19:37:27Z brian $
//
// Copyright (C) 2007 Burton Computer Corporation
// ALL RIGHTS RESERVED
//
// This program is open source software; you can redistribute it
// and/or modify it under the terms of the Q Public License (QPL)
// version 1.0. Use of this software in whole or in part, including
// linking it (modified or unmodified) into other programs is
// subject to the terms of the QPL.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// Q Public License for more details.
//
// You should have received a copy of the Q Public License
// along with this program; see the file LICENSE.txt.  If not, visit
// the Burton Computer Corporation or CoolDevTools web site
// QPL pages at:
//
//    http://www.burton-computer.com/qpl.html
//    http://www.cooldevtools.com/qpl.html
//

#include "HdlArgumentConstraint.h"
#include "HdlError.h"
#include "HdlToken.h"
#include "HdlStatement.h"
#include "HdlRecursiveStatementConstraint.h"
#include "HdlStandardStatementConstraint.h"
#include "HdlSyntaxChecker.h"

static void validate_token_name(const CRef<HdlStatement> &stmt,
                                const string &name)
{
  if (stmt->name()->strValue() != name) {
    throw HdlError(string("expected ") + name, stmt->name());
  }
}

HdlSyntaxChecker::HdlSyntaxChecker(const CRef<HdlStatement> &syntax_tree)
{
  initFromSyntaxTree(syntax_tree);
}

HdlSyntaxChecker::~HdlSyntaxChecker()
{
}

void HdlSyntaxChecker::checkSyntax(const CRef<HdlStatement> &stmt)
{
  m_constraint->validateStatement(stmt);
}

void HdlSyntaxChecker::initFromSyntaxTree(const CRef<HdlStatement> &stmt)
{
  validate_token_name(stmt, "language");

  verifyArgCount(stmt, 1);
  verifyArgType(stmt, 0, HdlToken::IDENTIFIER);

  Ref<HdlStandardStatementConstraint> syntax(new HdlStandardStatementConstraint(stmt->argument(0)->strValue(), true));
  for (int i = 0; i < stmt->numChildren(); ++i) {
    const CRef<HdlStatement> &child = stmt->child(i);
    string name = child->name()->strValue();
    if (name == "statement") {
      parseStatement(syntax, child);
    } else if (name == "block") {
      parseBlock(syntax, child);
    } else if (name == "unchecked") {
      parseUnchecked(syntax, child);
    } else {
      throw HdlError("expected unchecked, block, or statement", child->name());
    }
  }

  m_constraint = syntax;
}

int HdlSyntaxChecker::parseArgs(const Ref<HdlStandardStatementConstraint> &stmt_constraint,
                                const CRef<HdlStatement> &stmt,
                                int index)
{
  if (stmt->numChildren() < 1) {
    throw HdlError("expected args statement", stmt->name());
  }

  const CRef<HdlStatement> &args = stmt->child(0);
  validate_token_name(args, "args");

  if (args->numArguments() < 2) {
    throw HdlError("expected at least 2 arguments", args->name());
  }

  verifyArgType(args, 0, HdlToken::INTEGER);
  verifyArgType(args, 1, HdlToken::INTEGER);

  int min_args = args->argument(0)->intValue();
  int max_args = args->argument(1)->intValue();
  stmt_constraint->setArgCounts(min_args, max_args);

  int expected = min_args;
  if (max_args > min_args) {
    expected += 1;
  }

  if (args->numArguments() < (expected + 2)) {
    throw HdlError("not enough arguments to args statement", args->name());
  }

  vector<Ref<HdlArgumentConstraint> > tables;
  for (int i = 2; expected > 0; ++i) {
    const CRef<HdlToken> &arg = args->argument(i);
    Ref<HdlArgumentConstraint> arg_constraint(new HdlArgumentConstraint);
    if (arg->strValue() == "string") {
      arg_constraint->acceptString();
    } else if (arg->strValue() == "int") {
      arg_constraint->acceptInteger();
    } else if (arg->strValue() == "double") {
      arg_constraint->acceptDouble();
    } else if (arg->strValue() == "anyid") {
      arg_constraint->acceptID();
    } else if (arg->strValue() == "id") {
      i += 1;
      if (i >= args->numArguments()) {
        throw HdlError("identifier expected after id", args->name());
      }
      verifyArgType(args, i, HdlToken::IDENTIFIER);
      arg_constraint->acceptID(args->argument(i));
    } else if (arg->strValue() == "table") {
      tables.push_back(arg_constraint);
    } else {
      throw HdlError("expected string, int, anyid, id, or table", arg);
    }
    stmt_constraint->addArgument(arg_constraint);
    expected -= 1;
  }

  int expected_children = 1 + tables.size();
  if (stmt->numChildren() < expected_children) {
    throw HdlError("insufficient children for specified number of tables", stmt->name());
  }

  for (int i = 1; i < expected_children; ++i) {
    Ref<HdlArgumentConstraint> &table = tables[i - 1];
    const CRef<HdlStatement> &child = stmt->child(i);
    if (child->name()->strValue() != "table") {
      throw HdlError("expected table", child->name());
    }
    parseTable(table, child);
  }

  return expected_children;
}

void HdlSyntaxChecker::parseTable(const Ref<HdlArgumentConstraint> &table,
                                  const CRef<HdlStatement> &stmt)
{
  int id_index = 0;
  for (int i = 0; i < stmt->numArguments(); ++i) {
    const CRef<HdlToken> &token = stmt->argument(i);
    if (token->strValue() == "string") {
      table->acceptString();
    } else if (token->strValue() == "int") {
      table->acceptInteger();
    } else if (token->strValue() == "double") {
      table->acceptDouble();
    } else if (token->strValue() == "anyid") {
      table->acceptID();
    } else if (token->strValue() == "id") {
      i += 1;
      if (i >= stmt->numArguments()) {
        throw HdlError("expected identifier after id", stmt->name());
      }
      verifyArgType(stmt, i, HdlToken::IDENTIFIER);
      table->acceptID(stmt->argument(i));
    } else {
      throw HdlError("expected string, int, anyid, or id", token);
    }
  }
}

void HdlSyntaxChecker::parseBlock(const Ref<HdlStandardStatementConstraint> &stmt_constraint,
                                  const CRef<HdlStatement> &stmt)
{
  verifyIsBlock(stmt, true);
  if (stmt->numArguments() == 0) {
    throw HdlError("at least one argument expected", stmt->name());
  }
  verifyArgType(stmt, 0, HdlToken::IDENTIFIER);

  Ref<HdlStandardStatementConstraint> block(new HdlStandardStatementConstraint(stmt->argument(0)->strValue(), true));

  for (int i = 1; i < stmt->numArguments(); ++i) {
    const CRef<HdlToken> &arg = stmt->argument(i);
    if (arg->strValue() == "recursive") {
      block->addChild(Ref<HdlStatementConstraint>(new HdlRecursiveStatementConstraint(block.ptr())));
    } else {
      throw HdlError("unknown flag for block declaration", arg);
    }
  }

  for (int next_child = parseArgs(block, stmt, 0); next_child < stmt->numChildren(); ++next_child) {
    const CRef<HdlStatement> &child = stmt->child(next_child);
    string name = child->name()->strValue();
    if (name == "statement") {
      parseStatement(block, child);
    } else if (name == "block") {
      parseBlock(block, child);
    } else if (name == "unchecked") {
      parseUnchecked(block, child);
    } else {
      throw HdlError("expected unchecked, block, or statement", child->name());
    }
  }

  stmt_constraint->addChild(block);
}

void HdlSyntaxChecker::parseStatement(const Ref<HdlStandardStatementConstraint> &stmt_constraint,
                                      const CRef<HdlStatement> &stmt)
{
  verifyIsBlock(stmt, true);
  verifyArgCount(stmt, 1);
  verifyArgType(stmt, 0, HdlToken::IDENTIFIER);

  Ref<HdlStandardStatementConstraint> block(new HdlStandardStatementConstraint(stmt->argument(0)->strValue(), false));

  int next_child = parseArgs(block, stmt, 0);
  if (next_child != stmt->numChildren()) {
    throw HdlError("statement declarations can only contain arg and table statements", stmt->name());
  }

  stmt_constraint->addChild(block);
}

void HdlSyntaxChecker::parseUnchecked(const Ref<HdlStandardStatementConstraint> &stmt_constraint,
                                      const CRef<HdlStatement> &stmt)
{
  verifyIsBlock(stmt, false);
  verifyArgCount(stmt, 1);
  verifyArgType(stmt, 0, HdlToken::IDENTIFIER);
  stmt_constraint->addChild(Ref<HdlStatementConstraint>(new HdlStatementConstraint(stmt->argument(0)->strValue())));
}

void HdlSyntaxChecker::verifyArgCount(const CRef<HdlStatement> &stmt,
                                      int count)
{
  if (stmt->numArguments() != count) {
    throw HdlError(string("statement requires ") + num_to_string(count) + " arguments but has " + num_to_string(stmt->numArguments()),
                   stmt->name());
  }
}

void HdlSyntaxChecker::verifyArgEquals(const CRef<HdlStatement> &stmt,
                                       int index,
                                       const CRef<HdlToken> &token)
{
  if (!stmt->argument(index)->equals(token)) {
    throw HdlError(string("arg ") + num_to_string(index + 1) + " expected '" + token->strValue() + "'",
                   stmt->argument(index));
  }
}

void HdlSyntaxChecker::verifyArgType(const CRef<HdlStatement> &stmt,
                                     int index,
                                     HdlToken::TokenType type)
{
  if (stmt->argument(index)->tokenType() != type) {
    throw HdlError(string("arg ") + num_to_string(index + 1) + " expected '" + 
                   HdlToken::typeName(type) + " found " +
                   stmt->argument(index)->typeName(),
                   stmt->argument(index));
  }
}

void HdlSyntaxChecker::verifyIsBlock(const CRef<HdlStatement> &stmt,
                                     bool is_block)
{
  if (is_block != stmt->isBlock()) {
    throw HdlError(string("statement must") + (is_block ? " " : " not ") + "be a block", stmt->name());
  }
}


syntax highlighted by Code2HTML, v. 0.9.1