/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the Collabora Office project.
 *
 * 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/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   Licensed to the Apache Software Foundation (ASF) under one or more
 *   contributor license agreements. See the NOTICE file distributed
 *   with this work for additional information regarding copyright
 *   ownership. The ASF licenses this file to you under the Apache
 *   License, Version 2.0 (the "License"); you may not use this file
 *   except in compliance with the License. You may obtain a copy of
 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
 */

#include <sal/config.h>

#include <assert.h>
#include <stdio.h>
#include <string.h>

// DVO: always use standard headers:
#include <istream>
#include <sstream>

#include "hwpeq.h"
#include <sal/types.h>
#include <sal/macros.h>

#include <rtl/character.hxx>

/* @Man: change the hwp formula to LaTeX */

#define EQ_CASE 0x01    // case sensitive cmd
#define EQ_ENV  0x02    // equiv to latex environment
#define EQ_ATOP 0x04    // must revert order

static bool IS_WS(std::istream::int_type ch) {
    return ch != std::istream::traits_type::eof()
        && rtl::isAsciiWhiteSpace(
            static_cast<unsigned char>(
                std::istream::traits_type::to_char_type(ch)));
}

static bool IS_BINARY(std::istream::int_type ch) {
    return ch != std::istream::traits_type::eof()
        && strchr("+-<=>", std::istream::traits_type::to_char_type(ch));
}

// sub and sup script status
enum { SCRIPT_NONE, SCRIPT_SUB, SCRIPT_SUP, SCRIPT_ALL};

static int  eq_word(OString& outs, std::istream *strm, int script = SCRIPT_NONE);
static bool eq_sentence(OString& outs, std::istream *strm, const char *end = nullptr);

namespace {

struct hwpeq {
  const char    *key;       // hwp math keyword
  const char    *latex;     // corresponding latex keyword
  int           nargs;      // # of argument
  unsigned char flag;       // case sensitive?
};

}

const hwpeq eq_tbl[] = {
  { "!=",         "\\equiv ", 0,  0   },
  { "#",          "\\\\",     0,  0   },
  { "+-",         "\\pm ",    0,  0   },
  { "-+",         "\\mp ",    0,  0   },
  { "<=",         "\\leq ",   0,  0   },
  { "==",         "\\equiv ", 0,  0   },
  { ">=",         "\\geq ",   0,  0   },
  { "Pr",         nullptr,       0,  0   },
  { "^",          "^",        1,  0   },
  { "_",          "_",        1,  0   },
  { "`",          "\\;",      0,  0   },
  { "acute",      nullptr,       1,  0   },
  { "aleph",      nullptr,       0,  0   },
  { "alpha",      nullptr,       0,  EQ_CASE },
  { "amalg",      nullptr,       0,  0   },
  { "and",        nullptr,       0,  0   },
  { "angle",      nullptr,       0,  0   },
  { "angstrom",   nullptr,       0,  0   },
  { "approx",     nullptr,       0,  0   },
  { "arc",        nullptr,       0,  0   },
  { "arccos",     nullptr,       0,  0   },
  { "arch",       nullptr,       0,  0   },
  { "arcsin",     nullptr,       0,  0   },
  { "arctan",     nullptr,       0,  0   },
  { "arg",        nullptr,       0,  0   },
  { "assert",     "\\vdash",  0,  0   },
  { "ast",        nullptr,       0,  0   },
  { "asymp",      nullptr,       0,  0   },
  { "atop",       nullptr,       1,  EQ_ATOP },
  { "backslash",  nullptr,       0,  0   },
  { "bar",        nullptr,       1,  0   },
  { "because",    nullptr,       0,  0   },
  { "beta",       nullptr,       0,  EQ_CASE },
  { "big",        nullptr,       0,  EQ_CASE },
  { "bigcap",     nullptr,       0,  0   },
  { "bigcirc",    nullptr,       0,  0   },
  { "bigcup",     nullptr,       0,  0   },
  { "bigg",       nullptr,       0,  EQ_CASE },
  { "bigodiv",    nullptr,       0,  0   },
  { "bigodot",    nullptr,       0,  0   },
  { "bigominus",  nullptr,       0,  0   },
  { "bigoplus",   nullptr,       0,  0   },
  { "bigotimes",  nullptr,       0,  0   },
  { "bigsqcap",   nullptr,       0,  0   },
  { "bigsqcup",   nullptr,       0,  0   },
  { "biguplus",   nullptr,       0,  0   },
  { "bigvee",     nullptr,       0,  0   },
  { "bigwedge",   nullptr,       0,  0   },
  { "binom",      nullptr,       2,  0   },
  { "bmatrix",    nullptr,       0,  EQ_ENV  },
  { "bold",       nullptr,       0,  0   },
  { "bot",        nullptr,       0,  0   },
  { "breve",      nullptr,       1,  0   },
  { "buildrel",   nullptr,       0,  0   }, // LATER
  { "bullet",     nullptr,       0,  0   },
  { "cap",        nullptr,       0,  0   },
  { "cases",      nullptr,       0,  EQ_ENV  },
  { "ccol",       nullptr,       0,  0   }, /* Center vertically */
  { "cdot",       nullptr,       0,  0   },
  { "cdots",      nullptr,       0,  0   },
  { "check",      nullptr,       1,  0   },
  { "chi",        nullptr,       0,  EQ_CASE },
  { "choose",     nullptr,       0,  EQ_ATOP },
  { "circ",       nullptr,       0,  0   },
  { "col",        nullptr,       0,  0   }, // LATER
  { "cong",       nullptr,       0,  0   },
  { "coprod",     nullptr,       0,  0   },
  { "cos",        nullptr,       0,  0   },
  { "cosec",      nullptr,       0,  0   },
  { "cosh",       nullptr,       0,  0   },
  { "cot",        nullptr,       0,  0   },
  { "coth",       nullptr,       0,  0   },
  { "cpile",      nullptr,       0,  0   }, // LATER
  { "csc",        nullptr,       0,  0   },
  { "cup",        nullptr,       0,  0   },
  { "dagger",     nullptr,       0,  0   },
  { "dashv",      nullptr,       0,  0   },
  { "ddagger",    nullptr,       0,  0   },
  { "ddot",       nullptr,       1,  0   },
  { "ddots",      nullptr,       0,  0   },
  { "def",        nullptr,       0,  0   },
  { "deg",        nullptr,       0,  0   },
  { "del",        nullptr,       0,  0   },
  { "delta",      nullptr,       0,  EQ_CASE },
  { "diamond",    nullptr,       0,  0   },
  { "dim",        nullptr,       0,  0   },
  { "div",        nullptr,       0,  0   },
  { "divide",     nullptr,       0,  0   },
  { "dline",      nullptr,       0,  0   },
  { "dmatrix",    nullptr,       0,  EQ_ENV  },
  { "dot",        nullptr,       1,  0   },
  { "doteq",      nullptr,       0,  0   },
  { "dotsaxis",   nullptr,       0,  0   },
  { "dotsdiag",   nullptr,       0,  0   },
  { "dotslow",    "\\ldots",  0,  0   },
  { "dotsvert",   "\\vdots",  0,  0   },
  { "downarrow",  nullptr,       0,  EQ_CASE },
  { "dsum",       "+",        0,  0   },
  { "dyad",       nullptr,       0,  0   }, // LATER
  { "ell",        nullptr,       0,  0   },
  { "emptyset",   nullptr,       0,  0   },
  { "epsilon",    nullptr,       0,  EQ_CASE },
  { "eqalign",    nullptr,       0,  EQ_ENV  },
  { "equiv",      nullptr,       0,  0   },
  { "eta",        nullptr,       0,  EQ_CASE },
  { "exarrow",    nullptr,       0,  0   },
  { "exist",      "\\exists", 0,  0   },
  { "exists",     nullptr,       0,  0   },
  { "exp",        nullptr,       0,  EQ_CASE },
  { "for",        nullptr,       0,  0   },
  { "forall",     nullptr,       0,  0   },
  { "from",       "_",        1,  0   },
  { "gamma",      nullptr,       0,  EQ_CASE },
  { "gcd",        nullptr,       0,  0   },
  { "ge",         "\\geq",    0,  0   },
  { "geq",        nullptr,       0,  0   },
  { "ggg",        nullptr,       0,  0   },
  { "grad",       nullptr,       0,  0   },
  { "grave",      nullptr,       1,  0   },
  { "hat",        "\\widehat",    1,  0   },
  { "hbar",       nullptr,       0,  0   },
  { "hom",        nullptr,       0,  0   },
  { "hookleft",   nullptr,       0,  0   },
  { "hookright",  nullptr,       0,  0   },
  { "identical",  nullptr,       0,  0   }, // LATER
  { "if",         nullptr,       0,  0   },
  { "imag",       nullptr,       0,  0   },
  { "image",      nullptr,       0,  0   },
  { "imath",      nullptr,       0,  0   },
  { "in",         nullptr,       0,  0   },
  { "inf",        "\\infty",  0,  0   },
  { "infinity",   "\\infty",  0,  0   },
  { "infty",      nullptr,       0,  0   },
  { "int",        nullptr,       0,  0   },
  { "integral",   "\\int",    0,  0   },
  { "inter",      "\\bigcap", 0,  0   },
  { "iota",       nullptr,       0,  EQ_CASE },
  { "iso",        nullptr,       0,  0   }, // ams
  { "it",         nullptr,       0,  0   },
  { "jmath",      nullptr,       0,  0   },
  { "kappa",      nullptr,       0,  EQ_CASE },
  { "ker",        nullptr,       0,  0   },
  { "lambda",     nullptr,       0,  EQ_CASE },
  { "land",       nullptr,       0,  0   }, // LATER
  { "langle",     nullptr,       0,  0   },
  { "larrow",     "\\leftarrow",  0,  EQ_CASE },
  { "lbrace",     nullptr,       0,  0   },
  { "lbrack",     "[",        0,  0   },
  { "lceil",      nullptr,       0,  0   },
  { "lcol",       nullptr,       0,  0   }, // LATER
  { "ldots",      nullptr,       0,  0   },
  { "le",         nullptr,       0,  0   },
  { "left",       nullptr,       0,  0   },
  { "leftarrow",  nullptr,       0,  EQ_CASE },
  { "leq",        nullptr,       0,  0   },
  { "lfloor",     nullptr,       0,  0   },
  { "lg",         nullptr,       0,  0   },
  { "lim",        nullptr,       0,  EQ_CASE },
  { "line",       "\\vert",   0,  0   },
  { "liter",      "\\ell",    0,  0   },
  { "lll",        nullptr,       0,  0   }, // ams
  { "ln",         nullptr,       0,  0   },
  { "log",        nullptr,       0,  0   },
  { "lor",        "\\vee",    0,  0   },
  { "lparen",     "(",        0,  0   },
  { "lpile",      nullptr,       0,  0   }, // LATER
  { "lrarrow",    "\\leftrightarrow",   0,  EQ_CASE },
  { "lrharpoons", "\\leftrightharpoons",0,  0   },
  { "mapsto",     nullptr,       0,  0   },
  { "massert",    "\\dashv",  0,  0   },
  { "matrix",     nullptr,       0,  EQ_ENV  },
  { "max",        nullptr,       0,  0   },
  { "mho",        nullptr,       0,  0   }, // ams
  { "min",        nullptr,       0,  0   },
  { "minusplus",  nullptr,       0,  0   },
  { "mit",        "",     0,  0   }, // font
  { "mod",        "\\bmod",   0,  0   },
  { "models",     nullptr,       0,  0   },
  { "msangle",    nullptr,       0,  0   }, // LATER
  { "mu",         nullptr,       0,  EQ_CASE },
  { "nabla",      nullptr,       0,  0   },
  { "ne",         nullptr,       0,  0   },
  { "nearrow",    nullptr,       0,  0   },
  { "neg",        nullptr,       0,  0   },
  { "neq",        nullptr,       0,  0   },
  { "nequiv",     nullptr,       0,  0   },
  { "ni",         nullptr,       0,  0   },
  { "not",        nullptr,       0,  0   },
  { "notin",      nullptr,       0,  0   },
  { "nu",         nullptr,       0,  EQ_CASE },
  { "nwarrow",    nullptr,       0,  0   },
  { "odiv",       nullptr,       0,  0   },
  { "odot",       nullptr,       0,  0   },
  { "oint",       nullptr,       0,  0   },
  { "omega",      nullptr,       0,  EQ_CASE },
  { "omicron",    nullptr,       0,  EQ_CASE },
  { "ominus",     nullptr,       0,  0   },
  { "oplus",      nullptr,       0,  0   },
  { "or ",        nullptr,       0,  0   },
  { "oslash",     nullptr,       0,  0   },
  { "otimes",     nullptr,       0,  0   },
  { "over",       nullptr,       1,  EQ_ATOP },
  { "overline",   nullptr,       1,  0   },
  { "owns",       "\\ni",     0,  0   },
  { "parallel",   nullptr,       0,  0   },
  { "partial",    nullptr,       0,  0   },
  { "phantom",    nullptr,       0,  0   },
  { "phi",        nullptr,       0,  EQ_CASE },
  { "pi",         nullptr,       0,  EQ_CASE },
  { "pile",       nullptr,       0,  0   }, // LATER
  { "plusminus",  "\\pm",     0,  0   },
  { "pmatrix",    nullptr,       0,  EQ_ENV  },
  { "prec",       nullptr,       0,  0   },
  { "prep",       nullptr,       0,  0   },
  { "prime",      nullptr,       0,  0   },
  { "prod",       nullptr,       0,  0   },
  { "propto",     nullptr,       0,  0   },
  { "psi",        nullptr,       0,  EQ_CASE },
  { "rangle",     nullptr,       0,  0   },
  { "rarrow",     "\\rightarrow", 0,  EQ_CASE },
  { "rbrace",     "]",        0,  0   },
  { "rbrace",     nullptr,       0,  0   },
  { "rceil",      nullptr,       0,  0   },
  { "rcol",       nullptr,       0,  0   }, // LATER
  { "real",       "\\Re",     0,  0   },
  { "reimage",    nullptr,       0,  0   },
  { "rel",        nullptr,       0,  0   },
  { "rfloor",     nullptr,       0,  0   },
  { "rho",        nullptr,       0,  EQ_CASE },
  { "right",      nullptr,       0,  0   },
  { "rightarrow", nullptr,       0,  EQ_CASE },
  { "rlharpoons", nullptr,       0,  0   },
  { "rm",         nullptr,       0,  0   },
  { "root",       "\\sqrt",   1,  0   },
  { "rparen",     ")",        0,  0   },
  { "rpile",      nullptr,       0,  0   }, // LATER
  { "rtangle",    nullptr,       0,  0   },
  { "sangle",     nullptr,       0,  0   },
  { "scale",      nullptr,       0,  0   },
  { "searrow",    nullptr,       0,  0   },
  { "sec",        nullptr,       0,  0   },
  { "sigma",      nullptr,       0,  EQ_CASE },
  { "sim",        nullptr,       0,  0   },
  { "simeq",      nullptr,       0,  0   },
  { "sin",        nullptr,       0,  0   },
  { "sinh",       nullptr,       0,  0   },
  { "slash",      nullptr,       0,  0   },
  { "smallint",   nullptr,       0,  0   },
  { "smallinter", nullptr,       0,  0   },
  { "smalloint",  nullptr,       0,  0   },
  { "smallprod",  nullptr,       0,  0   },
  { "smallsum",   nullptr,       0,  0   },
  { "smallunion", nullptr,       0,  0   },
  { "smcoprod",   nullptr,       0,  0   },
  { "sqcap",      nullptr,       0,  0   },
  { "sqcup",      nullptr,       0,  0   },
  { "sqrt",       nullptr,       1,  0   },
  { "sqsubset",   nullptr,       0,  0   },
  { "sqsubseteq", nullptr,       0,  0   },
  { "sqsupset",   nullptr,       0,  0   },
  { "sqsupseteq", nullptr,       0,  0   },
  { "star",       nullptr,       0,  0   },
  { "sub",        "_",        0,  0   },
  { "subset",     nullptr,       0,  0   },
  { "subseteq",   nullptr,       0,  0   },
  { "succ",       nullptr,       0,  0   },
  { "sum",        nullptr,       0,  0   },
  { "sup",        "^",        0,  0   },
  { "superset",   nullptr,       0,  0   },
  { "supset",     nullptr,       0,  0   },
  { "supseteq",   nullptr,       0,  0   },
  { "swarrow",    nullptr,       0,  0   },
  { "tan",        nullptr,       0,  0   },
  { "tanh",       nullptr,       0,  0   },
  { "tau",        nullptr,       0,  EQ_CASE },
  { "therefore",  nullptr,       0,  0   },
  { "theta",      nullptr,       0,  EQ_CASE },
  { "tilde",      "\\widetilde",  1,  0   },
  { "times",      nullptr,       0,  0   },
  { "to",         "^",        1,  0   },
  { "top",        nullptr,       0,  0   },
  { "triangle",   nullptr,       0,  0   },
  { "triangled",  nullptr,       0,  0   },
  { "trianglel",  nullptr,       0,  0   },
  { "triangler",  nullptr,       0,  0   },
  { "triangleu",  nullptr,       0,  0   },
  { "udarrow",    "\\updownarrow",0,  EQ_CASE },
  { "under",      "\\underline",  1,  0   },
  { "underline",  "\\underline",  1,  0   },
  { "union",      "\\bigcup", 0,  0   },
  { "uparrow",    nullptr,       0,  EQ_CASE },
  { "uplus",      nullptr,       0,  0   },
  { "upsilon",    nullptr,       0,  EQ_CASE },
  { "varepsilon", nullptr,       0,  0   },
  { "varphi",     nullptr,       0,  0   },
  { "varpi",      nullptr,       0,  0   },
  { "varrho",     nullptr,       0,  0   },
  { "varsigma",   nullptr,       0,  0   },
  { "vartheta",   nullptr,       0,  0   },
  { "varupsilon", nullptr,       0,  0   },
  { "vdash",      nullptr,       0,  0   },
  { "vdots",      nullptr,       0,  0   },
  { "vec",        nullptr,       1,  0   },
  { "vee",        nullptr,       0,  0   },
  { "vert",       nullptr,       0,  0   },
  { "wedge",      nullptr,       0,  0   },
  { "wp",         nullptr,       0,  0   },
  { "xi",         nullptr,       0,  EQ_CASE },
  { "xor",        nullptr,       0,  0   },
  { "zeta",       nullptr,       0,  EQ_CASE }
};

static const hwpeq *lookup_eqn(char const *str)
{
  static const int eqCount = SAL_N_ELEMENTS(eq_tbl);
  int l = 0, r = eqCount;
  const hwpeq *result = nullptr;

  while( l < r ) {
    const int m = (l + r) / 2;
    const int k = strcmp(eq_tbl[m].key, str);
    if( k == 0 ) {
      result = eq_tbl + m;
      break;
    }
    else if( k < 0 )
      l = m + 1;
    else
      r = m;
  }
  return result;
}

/* If only the first character is uppercase or all characters are uppercase, change to lowercase */
static void make_keyword( char *keyword, std::string_view token)
{
    char* ptr;
    bool result = true;
    int len = token.length();
    assert(keyword);

    if( 255 < len )
    {
        len = 255;
    }
    memcpy(keyword, token.data(), len);
    keyword[len] = 0;

    if( (token[0] & 0x80) || rtl::isAsciiLowerCase(static_cast<unsigned char>(token[0])) || token.length() < 2 )
        return;

    bool capital = rtl::isAsciiUpperCase(
        static_cast<unsigned char>(keyword[1]));
    for( ptr = keyword + 2; *ptr && result; ptr++ )
    {
        if( (*ptr & 0x80) ||
            (!capital && rtl::isAsciiUpperCase(static_cast<unsigned char>(*ptr))) ||
            (capital && rtl::isAsciiLowerCase(static_cast<unsigned char>(*ptr))) )
        {
            result = false;
        }
    }

    if( result )
    {
        ptr = keyword;
        while( *ptr )
        {
            if( rtl::isAsciiUpperCase(static_cast<unsigned char>(*ptr)) )
                *ptr = sal::static_int_cast<char>(
                    rtl::toAsciiLowerCase(static_cast<unsigned char>(*ptr)));
            ptr++;
        }
    }
}

namespace {

// token reading function
struct eq_stack {
  OString  white;
  OString  token;
  std::istream   *strm;

  eq_stack() { strm = nullptr; };
  bool state(std::istream const *s) {
    if( strm != s) { white.clear(); token.clear(); }
    return token.getLength() != 0;
  }
};

}

static eq_stack *stk = nullptr;

static void push_token(OString const &white, OString const &token, std::istream *strm)
{
  // one time stack
  assert(stk->token.getLength() == 0);

  stk->white = white;
  stk->token = token;
  stk->strm = strm;
}

/*
 * It returns the length of the read tokens.
 *
 * control char, control sequence, binary sequence,
 * alphabet string, single character */
static int next_token(OString &white, OString &token, std::istream *strm)
{
  std::istream::int_type ch = 0;

  if( stk->state(strm) ) {
    white = stk->white;
    token = stk->token;
    stk->token.clear();
    stk->white.clear();
    return token.getLength();
  }

  token.clear();
  white.clear();
  if( !strm->good() )
    return 0;
  ch = strm->get();
  if( ch == std::istream::traits_type::eof() )
    return 0;

  // read preceding ws
  if( IS_WS(ch) ) {
    do
    {
        white += OStringChar(static_cast<char>(ch));
        ch = strm->get();
    } while (IS_WS(ch));
  }

  if( ch == '\\' || ch & 0x80
      || (ch != std::istream::traits_type::eof() && rtl::isAsciiAlpha(ch)) )
  {
    if( ch == '\\' ) {
      token += OStringChar(static_cast<char>(ch));
      ch = strm->get();
    }
    do {
      token += OStringChar(static_cast<char>(ch));
      ch = strm->get();
    } while( ch != std::istream::traits_type::eof()
             && (ch & 0x80 || rtl::isAsciiAlpha(ch)) ) ;
    strm->putback(static_cast<char>(ch));
    /* special treatment of sub, sub, over, atop
       The reason for this is that affect next_state().
     */
    if( token.equalsIgnoreAsciiCase("sub") || token.equalsIgnoreAsciiCase("from") ||
    token.equalsIgnoreAsciiCase("sup") || token.equalsIgnoreAsciiCase("to") ||
    token.equalsIgnoreAsciiCase("over") || token.equalsIgnoreAsciiCase("atop") ||
    token.equalsIgnoreAsciiCase("left") || token.equalsIgnoreAsciiCase("right") )
    {
      char buf[256];
      make_keyword(buf, token);
      token = OString(buf);
    }
    if( token == "sub" || token == "from" )
      token = "_"_ostr;
    if( token == "sup" || token == "to" )
      token = "^"_ostr;
  }
  else if( IS_BINARY(ch) ) {
    do
    {
        token += OStringChar(static_cast<char>(ch));
        ch = strm->get();
    }
    while( IS_BINARY(ch) );
    strm->putback(static_cast<char>(ch));
  }
  else if( ch != std::istream::traits_type::eof() && rtl::isAsciiDigit(ch) ) {
    do {
        token += OStringChar(static_cast<char>(ch));
        ch = strm->get();
    } while( ch != std::istream::traits_type::eof() && rtl::isAsciiDigit(ch) );
    strm->putback(static_cast<char>(ch));
  }
  else
    token += OStringChar(static_cast<char>(ch));

  return token.getLength();
}

static std::istream::int_type read_white_space(OString& outs, std::istream *strm)
{
  std::istream::int_type result;

  if( stk->state(strm) ) {
    outs += stk->white;
    stk->white.clear();
    result = std::istream::traits_type::to_int_type(stk->token[0]);
  }
  else {
    std::istream::int_type ch;
    for (;;)
    {
        ch = strm->get();
        if (!IS_WS(ch))
            break;
        outs += OStringChar(static_cast<char>(ch));
    }
    strm->putback(static_cast<char>(ch));
    result = ch;
  }
  return result;
}

/* If the argument is not required, delimiters are space and brace for each items.
   sqrt {ab} c = sqrt {ab} c
   (,} are for grouping
   ^ ,_ are for combination

   Sorting of formats with arguments, such as sqrt
      sqrt a -> sqrt {a}
      sqrt {a} -> sqrt {a}
   If there is more than one argument, it eliminates backslash between arguments.
      \frac a b -> frac {a} {b}
   Clean the form of over
      a over b -> {a} over {b}
 */

static int eq_word(OString& outs, std::istream *strm, int status)
{
  OString  token, white, state;
  int       result;
  char      keyword[256];
  const hwpeq *eq;

  next_token(white, token, strm);
  if (token.getLength() <= 0)
      return 0;
  result = token[0];

  if( token == "{" ) {
    state += white + token;
    eq_sentence(state, strm, "}");
  }
  else if( token == "left" ) {
    state += white + token;
    next_token(white, token, strm);
    state += white + token;

    eq_sentence(state, strm, "right");

    next_token(white, token, strm);
    state += white + token;
  }
  else {
    /* Normal token */
    int script_status = SCRIPT_NONE;
    while( true ) {
      state += white + token;
      make_keyword(keyword, token);
      if( token[0] == '^' )
        script_status |= SCRIPT_SUP;
      else if( token[0] == '_' )
        script_status |= SCRIPT_SUB;
      else
        script_status = SCRIPT_NONE;

      if( nullptr != (eq = lookup_eqn(keyword)) ) {
        int nargs = eq->nargs;
        while( nargs-- ) {
          const std::istream::int_type ch = read_white_space(state, strm);
          if( ch != '{' ) state += OStringChar('{');
          eq_word(state, strm, script_status);
          if( ch != '{' ) state += OStringChar('}');
        }
      }

      if( !next_token(white, token, strm) )
        break;
      // end loop and restart with this
      if( (token[0] == '^' && status && !(status & SCRIPT_SUP)) ||
          (token[0] == '_' && status && !(status & SCRIPT_SUB)) ||
          "over" == token || "atop" == token ||
          strchr("{}#&`", token[0]) ||
          (!strchr("^_", token[0]) && white.getLength()) )
      {
        push_token(white, token, strm);
        break;
      }
    }
  }
  outs += state;

  return result;
}

static bool eq_sentence(OString& outs, std::istream *strm, const char *end)
{
  OString  state;
  OString  white, token;
  bool      multiline = false;

  read_white_space(outs, strm);
  while( eq_word(state, strm) ) {
    if( !next_token(white, token, strm) ||
    (end && token == end) )
    {
      state += white + token;
      break;
    }
    push_token(white, token, strm);
    if( token == "atop" || token == "over" )
      outs += OStringChar('{') + state + OStringChar('}');
    else {
      if( token == "#" )
        multiline = true;
      outs += state;
    }
    state.clear();
    read_white_space(outs, strm);
  }
  outs += state;
  return multiline;
}

static char eq2ltxconv(OString& sstr, std::istream *strm, const char *sentinel)
{
  OString  white, token;
  char      key[256];
  std::istream::int_type ch;
  int       result;

  while( 0 != (result = next_token(white, token, strm)) ) {
    if( sentinel && (result == 1) && strchr(sentinel, token[0]) )
      break;
    make_keyword(key, token);
    const hwpeq *eq = nullptr;
    if( (eq = lookup_eqn(key)) != nullptr ) {
      const bool bUpperFollowingChar = ( (eq->flag & EQ_CASE)
          && rtl::isAsciiUpperCase(static_cast<unsigned char>(token[0])) );

      if( eq->latex )
        token = OString(eq->latex);
      else {
        token = OString::Concat("\\") + eq->key;
      }

      if (bUpperFollowingChar)
        token = token.replaceAt(1, 1, token.copy(1, 1).toAsciiUpperCase());
    }

    if( token[0] == '{' ) { // grouping
      sstr += white + token;
      eq2ltxconv(sstr, strm, "}");
      sstr += OStringChar('}');
    }
    else if( eq && (eq->flag & EQ_ENV) ) {
      next_token(white, token, strm);
      if( token[0] != '{' )
        return 0;
      sstr += OString::Concat("\\begin{") + eq->key + "}" SAL_NEWLINE_STRING ;
      eq2ltxconv(sstr, strm, "}");
      if( sstr[sstr.getLength() - 1] != '\n' )
        sstr += SAL_NEWLINE_STRING ;
      sstr += OString::Concat("\\end{") + eq->key + "}" SAL_NEWLINE_STRING ;
    }
    else if( eq && (eq->flag & EQ_ATOP) ) {
      if( sstr.getLength() == 0 )
        sstr += OStringChar('{');
      else {
        int pos  = sstr.lastIndexOf('}');
        if( 0 < pos)
          sstr = sstr.replaceAt(pos, 1, " ");
      }
      sstr += token;
      for (;;)
      {
        ch = strm->get();
        if ( ch == std::istream::traits_type::eof() || !IS_WS(ch) )
            break;
        sstr += OStringChar(static_cast<char>(ch));
      }
      if( ch != '{' )
        sstr += "{}";
      else {
        eq2ltxconv(sstr, strm, "}");
        sstr += OStringChar('}');
      }
    }
    else
      sstr += white + token;
  }
  return token[0];
}

void eq2latex(OString& outs, char const *s)
{
  assert(s);
  if( stk == nullptr )
    stk = new eq_stack;

  OString  tstr;

  std::istringstream tstrm(s);
  bool eqnarray = eq_sentence(tstr, &tstrm);
  std::istringstream strm((std::string(tstr)));

  if( eqnarray )
    outs += "\\begin{array}{rllll}" SAL_NEWLINE_STRING;
  eq2ltxconv(outs, &strm, nullptr);
  outs += SAL_NEWLINE_STRING;
  if( eqnarray )
    outs += "\\end{array}" SAL_NEWLINE_STRING;
  delete stk;
  stk = nullptr;
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
