/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* utilities for regression tests based on frame tree comparison */
 
#include "nsIFrameUtil.h"
#include "nsFrame.h"
#include "nsString.h"
#include "nsRect.h"
#include <stdlib.h>
#include "plstr.h"


#ifdef DEBUG
class nsFrameUtil : public nsIFrameUtil {
protected:
  virtual ~nsFrameUtil();

public:
  nsFrameUtil();

  NS_DECL_ISUPPORTS

  NS_IMETHOD CompareRegressionData(FILE* aFile1, FILE* aFile2,int32_t aRegressionOutput=0) override;
  NS_IMETHOD DumpRegressionData(FILE* aInputFile, FILE* aOutputFile) override;

  struct Node;
  struct Tag;

  struct NodeList {
    NodeList();
    ~NodeList();

    static void Destroy(NodeList* aLists);

    NodeList* next;            // for lists of lists
    Node* node;
    char* name;
  };

  struct Node {
    Node();
    ~Node();

    static void Destroy(Node* aNode);

    static Node* Read(FILE* aFile, Tag* aTag);

    static Node* ReadTree(FILE* aFile);

    Node* next;
    char* type;
    uint32_t state;
    nsRect bbox;
    nsCString styleData;
    NodeList* lists;
  };

  struct Tag {
    Tag();
    ~Tag();

    static Tag* Parse(FILE* aFile);

    void AddAttr(char* aAttr, char* aValue);

    const char* GetAttr(const char* aAttr);

    void ReadAttrs(FILE* aFile);

    void ToString(nsString& aResult);

    enum Type {
      open,
      close,
      openClose
    };

    char* name;
    Type type;
    char** attributes;
    int32_t num;
    int32_t size;
    char** values;
  };

  static char* Copy(const char* aString);

  static void DumpNode(Node* aNode, FILE* aOutputFile, int32_t aIndent);
  static void DumpTree(Node* aNode, FILE* aOutputFile, int32_t aIndent);
  static bool CompareTrees(Node* aNode1, Node* aNode2);
};

char*
nsFrameUtil::Copy(const char* aString)
{
  if (aString) {
    int l = ::strlen(aString);
    char* c = new char[l+1];
    if (!c)
      return nullptr;
    memcpy(c, aString, l+1);
    return c;
  }
  return nullptr;
}

//----------------------------------------------------------------------

nsFrameUtil::NodeList::NodeList()
  : next(nullptr), node(nullptr), name(nullptr)
{
}

nsFrameUtil::NodeList::~NodeList()
{
  if (nullptr != name) {
    delete name;
  }
  if (nullptr != node) {
    Node::Destroy(node);
  }
}

void
nsFrameUtil::NodeList::Destroy(NodeList* aLists)
{
  while (nullptr != aLists) {
    NodeList* next = aLists->next;
    delete aLists;
    aLists = next;
  }
}

//----------------------------------------------------------------------

nsFrameUtil::Node::Node()
  : next(nullptr), type(nullptr), state(0), lists(nullptr)
{
}

nsFrameUtil::Node::~Node()
{
  if (nullptr != type) {
    delete type;
  }
  if (nullptr != lists) {
    NodeList::Destroy(lists);
  }
}

void
nsFrameUtil::Node::Destroy(Node* aList)
{
  while (nullptr != aList) {
    Node* next = aList->next;
    delete aList;
    aList = next;
  }
}

static int32_t GetInt(nsFrameUtil::Tag* aTag, const char* aAttr)
{
  const char* value = aTag->GetAttr(aAttr);
  if (nullptr != value) {
    return int32_t( atoi(value) );
  }
  return 0;
}

nsFrameUtil::Node*
nsFrameUtil::Node::ReadTree(FILE* aFile)
{
  Tag* tag = Tag::Parse(aFile);
  if (nullptr == tag) {
    return nullptr;
  }
  if (PL_strcmp(tag->name, "frame") != 0) {
    delete tag;
    return nullptr;
  }
  Node* result = Read(aFile, tag);
  fclose(aFile);
  return result;
}

nsFrameUtil::Node*
nsFrameUtil::Node::Read(FILE* aFile, Tag* tag)
{
  Node* node = new Node;
  node->type = Copy(tag->GetAttr("type"));
  if (!node->type) {
    /* crash() */
  }
  node->state = GetInt(tag, "state");
  delete tag;

  for (;;) {
    tag = Tag::Parse(aFile);
    if (nullptr == tag) break;
    if (PL_strcmp(tag->name, "frame") == 0) {
      delete tag;
      break;
    }
    if (PL_strcmp(tag->name, "bbox") == 0) {
      nscoord x = nscoord( GetInt(tag, "x") );
      nscoord y = nscoord( GetInt(tag, "y") );
      nscoord w = nscoord( GetInt(tag, "w") );
      nscoord h = nscoord( GetInt(tag, "h") );
      node->bbox.SetRect(x, y, w, h);
    }
    else if (PL_strcmp(tag->name, "child-list") == 0) {
      NodeList* list = new NodeList();
      list->name = Copy(tag->GetAttr("name"));
      if (!list->name) {
        /* crash() */
      }
      list->next = node->lists;
      node->lists = list;
      delete tag;

      Node** tailp = &list->node;
      for (;;) {
        tag = Tag::Parse(aFile);
        if (nullptr == tag) {
          break;
        }
        if (PL_strcmp(tag->name, "child-list") == 0) {
          break;
        }
        if (PL_strcmp(tag->name, "frame") != 0) {
          break;
        }
        Node* child = Node::Read(aFile, tag);
        if (nullptr == child) {
          break;
        }
        *tailp = child;
        tailp = &child->next;
      }
    }
    else if((PL_strcmp(tag->name, "font") == 0) ||
            (PL_strcmp(tag->name, "color") == 0) ||
            (PL_strcmp(tag->name, "spacing") == 0) ||
            (PL_strcmp(tag->name, "list") == 0) ||
            (PL_strcmp(tag->name, "position") == 0) ||
            (PL_strcmp(tag->name, "text") == 0) ||
            (PL_strcmp(tag->name, "display") == 0) ||
            (PL_strcmp(tag->name, "table") == 0) ||
            (PL_strcmp(tag->name, "content") == 0) ||
            (PL_strcmp(tag->name, "UI") == 0) ||
            (PL_strcmp(tag->name, "print") == 0)) {
      const char* attr = tag->GetAttr("data");
      node->styleData.Append('|');
      node->styleData.Append(attr ? attr : "null attr");
    }

    delete tag;
  }
  return node;
}

//----------------------------------------------------------------------

nsFrameUtil::Tag::Tag()
  : name(nullptr), type(open), attributes(nullptr), num(0), size(0),
    values(nullptr)
{
}

nsFrameUtil::Tag::~Tag()
{
  int32_t i, n = num;
  if (0 != n) {
    for (i = 0; i < n; i++) {
      delete attributes[i];
      delete values[i];
    }
    delete attributes;
    delete values;
  }
}

void
nsFrameUtil::Tag::AddAttr(char* aAttr, char* aValue)
{
  if (num == size) {
    int32_t newSize = size * 2 + 4;
    char** a = new char*[newSize];
    char** v = new char*[newSize];
    if (0 != num) {
      memcpy(a, attributes, num * sizeof(char*));
      memcpy(v, values, num * sizeof(char*));
      delete attributes;
      delete values;
    }
    attributes = a;
    values = v;
    size = newSize;
  }
  attributes[num] = aAttr;
  values[num] = aValue;
  num = num + 1;
}

const char*
nsFrameUtil::Tag::GetAttr(const char* aAttr)
{
  int32_t i, n = num;
  for (i = 0; i < n; i++) {
    if (PL_strcmp(attributes[i], aAttr) == 0) {
      return values[i];
    }
  }
  return nullptr;
}

static inline int IsWhiteSpace(int c) {
  return (c == ' ') || (c == '\t') || (c == '\n') || (c == '\r');
}

static bool EatWS(FILE* aFile)
{
  for (;;) {
    int c = getc(aFile);
    if (c < 0) {
      return false;
    }
    if (!IsWhiteSpace(c)) {
      ungetc(c, aFile);
      break;
    }
  }
  return true;
}

static bool Expect(FILE* aFile, char aChar)
{
  int c = getc(aFile);
  if (c < 0) return false;
  if (c != aChar) {
    ungetc(c, aFile);
    return false;
  }
  return true;
}

static char* ReadIdent(FILE* aFile)
{
  char id[1000];
  char* ip = id;
  char* end = ip + sizeof(id) - 1;
  while (ip < end) {
    int c = fgetc(aFile);
    if (c < 0) return nullptr;
    if ((c == '=') || (c == '>') || (c == '/') || IsWhiteSpace(c)) {
      ungetc(c, aFile);
      break;
    }
    *ip++ = char(c);
  }
  *ip = '\0';
  return nsFrameUtil::Copy(id);
  /* may return a null pointer */
}

static char* ReadString(FILE* aFile)
{
  if (!Expect(aFile, '\"')) {
    return nullptr;
  }
  char id[1000];
  char* ip = id;
  char* end = ip + sizeof(id) - 1;
  while (ip < end) {
    int c = fgetc(aFile);
    if (c < 0) return nullptr;
    if (c == '\"') {
      break;
    }
    *ip++ = char(c);
  }
  *ip = '\0';
  return nsFrameUtil::Copy(id);
  /* may return a null pointer */
}

void
nsFrameUtil::Tag::ReadAttrs(FILE* aFile)
{
  for (;;) {
    if (!EatWS(aFile)) {
      break;
    }
    int c = getc(aFile);
    if (c < 0) break;
    if (c == '/') {
      if (!EatWS(aFile)) {
        return;
      }
      if (Expect(aFile, '>')) {
        type = openClose;
        break;
      }
    }
    else if (c == '>') {
      break;
    }
    ungetc(c, aFile);
    char* attr = ReadIdent(aFile);
    if ((nullptr == attr) || !EatWS(aFile)) {
      break;
    }
    char* value = nullptr;
    if (Expect(aFile, '=')) {
      value = ReadString(aFile);
      if (nullptr == value) {
        delete [] attr;
        break;
      }
    }
    AddAttr(attr, value);
  }
}

nsFrameUtil::Tag*
nsFrameUtil::Tag::Parse(FILE* aFile)
{
  if (!EatWS(aFile)) {
    return nullptr;
  }
  if (Expect(aFile, '<')) {
    Tag* tag = new Tag;
    if (Expect(aFile, '/')) {
      tag->type = close;
    }
    else {
      tag->type = open;
    }
    tag->name = ReadIdent(aFile);
    tag->ReadAttrs(aFile);
    return tag;
  }
  return nullptr;
}

void
nsFrameUtil::Tag::ToString(nsString& aResult)
{
  aResult.Truncate();
  aResult.Append(char16_t('<'));
  if (type == close) {
    aResult.Append(char16_t('/'));
  }
  aResult.AppendASCII(name);
  if (0 != num) {
    int32_t i, n = num;
    for (i = 0; i < n; i++) {
      aResult.Append(char16_t(' '));
      aResult.AppendASCII(attributes[i]);
      if (values[i]) {
        aResult.AppendLiteral("=\"");
        aResult.AppendASCII(values[i]);
        aResult.Append(char16_t('\"'));
      }
    }
  }
  if (type == openClose) {
    aResult.Append(char16_t('/'));
  }
  aResult.Append(char16_t('>'));
}

//----------------------------------------------------------------------

nsresult
NS_NewFrameUtil(nsIFrameUtil** aResult)
{
  NS_PRECONDITION(nullptr != aResult, "null pointer");
  if (nullptr == aResult) {
    return NS_ERROR_NULL_POINTER;
  }

  nsFrameUtil* it = new nsFrameUtil();

  NS_ADDREF(*aResult = it);
  return NS_OK;
}

nsFrameUtil::nsFrameUtil()
{
}

nsFrameUtil::~nsFrameUtil()
{
}

NS_IMPL_ISUPPORTS(nsFrameUtil, nsIFrameUtil)

void
nsFrameUtil::DumpNode(Node* aNode, FILE* aOutputFile, int32_t aIndent)
{
  nsFrame::IndentBy(aOutputFile, aIndent);
  fprintf(aOutputFile, "%s 0x%x %d,%d,%d,%d, %s\n", aNode->type, aNode->state,
          aNode->bbox.x, aNode->bbox.y,
          aNode->bbox.width, aNode->bbox.height,
          aNode->styleData.get());
}

void
nsFrameUtil::DumpTree(Node* aNode, FILE* aOutputFile, int32_t aIndent)
{
  while (nullptr != aNode) {
    DumpNode(aNode, aOutputFile, aIndent);
    nsFrameUtil::NodeList* lists = aNode->lists;
    if (nullptr != lists) {
      while (nullptr != lists) {
        nsFrame::IndentBy(aOutputFile, aIndent);
        fprintf(aOutputFile, " list: %s\n",
                lists->name ? lists->name : "primary");
        DumpTree(lists->node, aOutputFile, aIndent + 1);
        lists = lists->next;
      }
    }
    aNode = aNode->next;
  }
}

bool
nsFrameUtil::CompareTrees(Node* tree1, Node* tree2)
{
  bool result = true;
  for (;; tree1 = tree1->next, tree2 = tree2->next) {
    // Make sure both nodes are non-null, or at least agree with each other
    if (nullptr == tree1) {
      if (nullptr == tree2) {
        break;
      }
      printf("first tree prematurely ends\n");
      return false;
    }
    else if (nullptr == tree2) {
      printf("second tree prematurely ends\n");
      return false;
    }

    // Check the attributes that we care about
    if (0 != PL_strcmp(tree1->type, tree2->type)) {
      printf("frame type mismatch: %s vs. %s\n", tree1->type, tree2->type);
      printf("Node 1:\n");
      DumpNode(tree1, stdout, 1);
      printf("Node 2:\n");
      DumpNode(tree2, stdout, 1);
      return false;
    }

    // Ignore the XUL scrollbar frames
    static const char kScrollbarFrame[] = "ScrollbarFrame";
    if (0 == PL_strncmp(tree1->type, kScrollbarFrame, sizeof(kScrollbarFrame) - 1))
      continue;

    if (tree1->state != tree2->state) {
      printf("frame state mismatch: 0x%x vs. 0x%x\n",
             tree1->state, tree2->state);
      printf("Node 1:\n");
      DumpNode(tree1, stdout, 1);
      printf("Node 2:\n");
      DumpNode(tree2, stdout, 1);
      result = false; // we have a non-critical failure, so remember that but continue
    }
    if (tree1->bbox.IsEqualInterior(tree2->bbox)) {
      printf("frame bbox mismatch: %d,%d,%d,%d vs. %d,%d,%d,%d\n",
             tree1->bbox.x, tree1->bbox.y,
             tree1->bbox.width, tree1->bbox.height,
             tree2->bbox.x, tree2->bbox.y,
             tree2->bbox.width, tree2->bbox.height);
      printf("Node 1:\n");
      DumpNode(tree1, stdout, 1);
      printf("Node 2:\n");
      DumpNode(tree2, stdout, 1);
      result = false; // we have a non-critical failure, so remember that but continue
    }
    if (tree1->styleData != tree2->styleData) {
      printf("frame style data mismatch: %s vs. %s\n",
        tree1->styleData.get(),
        tree2->styleData.get());
    }

    // Check child lists too
    NodeList* list1 = tree1->lists;
    NodeList* list2 = tree2->lists;
    for (;;) {
      if (nullptr == list1) {
        if (nullptr != list2) {
          printf("first tree prematurely ends (no child lists)\n");
          printf("Node 1:\n");
          DumpNode(tree1, stdout, 1);
          printf("Node 2:\n");
          DumpNode(tree2, stdout, 1);
          return false;
        }
        else {
          break;
        }
      }
      if (nullptr == list2) {
        printf("second tree prematurely ends (no child lists)\n");
        printf("Node 1:\n");
        DumpNode(tree1, stdout, 1);
        printf("Node 2:\n");
        DumpNode(tree2, stdout, 1);
        return false;
      }
      if (0 != PL_strcmp(list1->name, list2->name)) {
        printf("child-list name mismatch: %s vs. %s\n",
               list1->name ? list1->name : "(null)",
               list2->name ? list2->name : "(null)");
        result = false; // we have a non-critical failure, so remember that but continue
      }
      else {
        bool equiv = CompareTrees(list1->node, list2->node);
        if (!equiv) {
          return equiv;
        }
      }
      list1 = list1->next;
      list2 = list2->next;
    }
  }
  return result;
}

NS_IMETHODIMP
nsFrameUtil::CompareRegressionData(FILE* aFile1, FILE* aFile2,int32_t aRegressionOutput)
{
  Node* tree1 = Node::ReadTree(aFile1);
  Node* tree2 = Node::ReadTree(aFile2);

  nsresult rv = NS_OK;
  if (!CompareTrees(tree1, tree2)) {
    // only output this if aRegressionOutput is 0
    if( 0 == aRegressionOutput ){
      printf("Regression data 1:\n");
      DumpTree(tree1, stdout, 0);
      printf("Regression data 2:\n");
      DumpTree(tree2, stdout, 0);
    }
    rv = NS_ERROR_FAILURE;
  }

  Node::Destroy(tree1);
  Node::Destroy(tree2);

  return rv;
}

NS_IMETHODIMP
nsFrameUtil::DumpRegressionData(FILE* aInputFile, FILE* aOutputFile)
{
  Node* tree1 = Node::ReadTree(aInputFile);
  if (nullptr != tree1) {
    DumpTree(tree1, aOutputFile, 0);
    Node::Destroy(tree1);
    return NS_OK;
  }
  return NS_ERROR_FAILURE;
}
#endif
