content: {-# LANGUAGE LambdaCase #-}
  {-# LANGUAGE OverloadedStrings #-}
  
  {-
      BNF Converter: Java Pretty Printer generator
      Copyright (C) 2004  Author:  Michael Pellauer, Bjorn Bringert
  
      Description   : This module generates the Java Pretty Printer
                      class. In addition, since there's no good way
                      to display a class heirarchy (toString() doesn't
                      count) in Java, it generates a method that
                      displays the Abstract Syntax in a way similar
                      to Haskell.
  
                      This uses Appel's method and may serve as a
                      useful example to those who wish to use it.
  
      Author        : Michael Pellauer (pellauer@cs.chalmers.se),
                      Bjorn Bringert (bringert@cs.chalmers.se)
  
      Created       : 24 April, 2003
  
      Modified      : 9 Aug, 2004
      Added string buffer for efficiency (Michael, August 03)
  
  -}
  
  module BNFC.Backend.Java.CFtoJavaPrinter15 ( cf2JavaPrinter ) where
  
  import Prelude hiding ((<>))
  
  import Data.Bifunctor ( second )
  import Data.Char      ( toLower, isSpace )
  import Data.Either    ( lefts )
  import Data.List      ( intersperse )
  
  import BNFC.CF
  import BNFC.PrettyPrint
  import BNFC.Utils     ( (+++), for, unless, unlessNull, uniqOn )
  
  import BNFC.Backend.Common ( switchByPrecedence )
  import BNFC.Backend.Common.NamedVariables
  import BNFC.Backend.Java.CFtoJavaAbs15
  
  --Produces the PrettyPrinter class.
  --It will generate two methods "print" and "show"
  --print is the actual pretty printer for linearization.
  --show produces a Haskell-style syntax that can be extremely useful
  --especially for testing parser correctness.
  
  cf2JavaPrinter :: String -> String -> CF -> String
  cf2JavaPrinter packageBase packageAbsyn cf =
    unlines
     [
      header,
      prEntryPoints packageAbsyn cf,
      unlines (map (prData packageAbsyn user) groups),
      unlines (map (shData packageAbsyn user) groups),
      footer
     ]
    where
      user = [n | (n,_) <- tokenPragmas cf]
      groups = fixCoercions (ruleGroupsInternals cf)
      header = unlines [
        "package" +++ packageBase ++ ";",
        "",
        "public class PrettyPrinter",
        "{",
        "  //For certain applications increasing the initial size of the buffer may improve performance.",
        "  private static final int INITIAL_BUFFER_SIZE = 128;",
        "  private static final int INDENT_WIDTH = 2;",
        "  //You may wish to change the parentheses used in precedence.",
        "  private static final String _L_PAREN = new String(\"(\");",
        "  private static final String _R_PAREN = new String(\")\");",
        prRender
        ]
      footer = unlines [ --later only include used categories
        "  private static void pp(Integer n, int _i_) { buf_.append(n); buf_.append(\" \"); }",
        "  private static void pp(Double d, int _i_) { buf_.append(String.format(java.util.Locale.ROOT, \"%.15g \", d)); }",
        "  private static void pp(String s, int _i_) { buf_.append(s); buf_.append(\" \"); }",
        "  private static void pp(Character c, int _i_) { buf_.append(\"'\" + escape(c.toString()) + \"'\"); buf_.append(\" \"); }",
        "  private static void sh(Integer n) { render(n.toString()); }",
        "  private static void sh(Double d) { render(String.format(java.util.Locale.ROOT, \"%.15g\", d)); }",
        "  private static void sh(Character c) { render(\"'\" + escape(c.toString()) + \"'\"); }",
        "  private static void sh(String s) { printQuoted(s); }",
        "",
        "  private static void printQuoted(String s) { render(\"\\\"\" + escape(s) + \"\\\"\"); }",
        "",
        "  public static String escape(String s) {",
        "    if (s == null) return null;",
        "    return s.replace(\"\\\\\", \"\\\\\\\\\")",
        "            .replace(\"\\t\", \"\\\\t\")",
        "            .replace(\"\\b\", \"\\\\b\")",
        "            .replace(\"\\n\", \"\\\\n\")",
        "            .replace(\"\\r\", \"\\\\r\")",
        "            .replace(\"\\f\", \"\\\\f\")",
        "            .replace(\"\\\"\", \"\\\\\\\"\");",
        "  }",
        "",
        "  private static void indent()",
        "  {",
        "    int n = _n_;",
        "    while (n > 0)",
        "    {",
        "      buf_.append(\' \');",
        "      n--;",
        "    }",
        "  }",
        "",
        "  private static void backup()",
        "  {",
        "    int prev = buf_.length() - 1;",
        "    if (prev >= 0 && buf_.charAt(prev) == ' ')",
        "      buf_.setLength(prev);",
        "  }",
        "",
        "  private static void trim()",
        "  {",
        "    // Trim initial spaces",
        "    int end = 0;",
        "    int len = buf_.length();",
        "    while (end < len && buf_.charAt(end) == ' ')",
        "      end++; ",
        "    buf_.delete(0, end);",
        "    // Trim trailing spaces",
        "    removeTrailingSpaces();",
        "  }",
        "",
        "  private static void removeTrailingSpaces()",
        "  {",
        "    int end = buf_.length();",
        "    while (end > 0 && buf_.charAt(end-1) == ' ')",
        "      end--;",
        "    buf_.setLength(end);",
        "  }",
        "",
        "  private static void removeTrailingWhitespace()",
        "  {",
        "    int end = buf_.length();",
        "    while (end > 0 && (buf_.charAt(end-1) == ' ' || buf_.charAt(end-1) == '\\n'))",
        "      end--;",
        "    buf_.setLength(end);",
        "  }",
        "",
        "  private static void onEmptyLine()",
        "  {",
        "    removeTrailingSpaces();",
        "    int len = buf_.length();",
        "    if (len > 0 && buf_.charAt(len-1) != '\\n') buf_.append(\"\\n\");",
        "    indent();",
        "  }",
        "",
        "  private static int _n_ = 0;",
        "  private static StringBuilder buf_ = new StringBuilder(INITIAL_BUFFER_SIZE);",
        "}"
        ]
  
  --An extremely simple renderer for terminals.
  prRender :: String
  prRender = unlines
    [
        "  //You may wish to change render",
        "  private static void render(String s)",
        "  {",
        "    if (s.equals(\"{\"))",
        "    {",
        "       onEmptyLine();",
        "       buf_.append(s);",
        "       _n_ = _n_ + INDENT_WIDTH;",
        "       buf_.append(\"\\n\");",
        "       indent();",
        "    }",
        "    else if (s.equals(\"(\") || s.equals(\"[\"))",
        "       buf_.append(s);",
        "    else if (s.equals(\")\") || s.equals(\"]\"))",
        "    {",
        "       removeTrailingWhitespace();",
        "       buf_.append(s);",
        "       buf_.append(\" \");",
        "    }",
        "    else if (s.equals(\"}\"))",
        "    {",
        "       _n_ = _n_ - INDENT_WIDTH;",
        "       onEmptyLine();",
        "       buf_.append(s);",
        "       buf_.append(\"\\n\");",
        "       indent();",
        "    }",
        "    else if (s.equals(\",\"))",
        "    {",
        "       removeTrailingWhitespace();",
        "       buf_.append(s);",
        "       buf_.append(\" \");",
        "    }",
        "    else if (s.equals(\";\"))",
        "    {",
        "       removeTrailingWhitespace();",
        "       buf_.append(s);",
        "       buf_.append(\"\\n\");",
        "       indent();",
        "    }",
        "    else if (s.equals(\"\")) return;",
        "    else if (s.trim().equals(\"\"))",
        "    {",
        "       backup();",
        "       buf_.append(s);",
        "    }",
        "    else",
        "    {",
        "       buf_.append(s);",
        "       buf_.append(\" \");",
        "    }",
        "  }"
    ]
  
  prEntryPoints :: String -> CF -> String
  prEntryPoints packageAbsyn cf =
      msg ++ concatMap prEntryPoint (allCatsNorm cf) ++ msg2
   where
    msg = "  //  print and show methods are defined for each category.\n"
    msg2 = "  /***   You shouldn't need to change anything beyond this point.   ***/\n"
    prEntryPoint cat = unlines
     [
      "  public static String print(" ++ packageAbsyn ++ "." ++ cat' ++ " foo)",
      "  {",
      "    pp(foo, 0);",
      "    trim();",
      "    String temp = buf_.toString();",
      "    buf_.delete(0,buf_.length());",
      "    return temp;",
      "  }",
      "  public static String show(" ++ packageAbsyn ++ "." ++ cat' ++ " foo)",
      "  {",
      "    sh(foo);",
      "    String temp = buf_.toString();",
      "    buf_.delete(0,buf_.length());",
      "    return temp;",
      "  }"
     ]
     where
      cat' = identCat cat
  
  prData :: String ->  [UserDef] -> (Cat, [Rule]) -> String
  prData packageAbsyn user (cat, rules)
    | isList cat = unlines $ concat
        [ [ "  private static void pp(" ++ packageAbsyn ++ "." ++ dat ++ " foo, int _i_)"
          , "  {"
          , "    pp" ++ dat ++ "(foo.iterator(), _i_);"
          , "  }"
          , ""
          , "  private static void pp" ++ dat ++ "(java.util.Iterator<" ++ et ++ "> it, int _i_)"
          , "  {"
          ]
        , map ("    " ++) $ prList dat et rules
        , [ "  }"
          , ""
          ]
        ]
    | otherwise = unlines $
        [ "  private static void pp(" ++ packageAbsyn ++ "."
             ++ dat +++ "foo, int _i_)"
        , "  {"
        , concat (addElse $ map (prRule packageAbsyn) rules)
        , "  }"
        ]
    where
    dat = identCat (normCat cat)
    et  = typename packageAbsyn user $ identCat $ normCatOfList cat
    addElse = map ("    " ++) . intersperse "else " . filter (not . null)
      . map (dropWhile isSpace)
  
  
  prRule :: String -> Rule -> String
  prRule packageAbsyn r@(Rule f _c cats _) | not (isCoercion f || isDefinedRule f) = concat
      [ "    if (foo instanceof" +++ packageAbsyn ++ "." ++ fun ++ ")\n"
      , "    {\n"
      , "       " ++ packageAbsyn ++ "." ++ fun +++ fnm +++ "= ("
          ++ packageAbsyn ++ "." ++ fun ++ ") foo;\n"
      , lparen
      , cats'
      , rparen
      , "    }\n"
      ]
    where
      fun = funName f
      p = precRule r
      (lparen, rparen) =
          ("       if (_i_ > " ++ show p ++ ") render(_L_PAREN);\n",
          "       if (_i_ > " ++ show p ++ ") render(_R_PAREN);\n")
      cats' = case cats of
          [] -> ""
          _  -> concatMap (render . prItem (text fnm)) (numVars cats)
      fnm = '_' : map toLower fun
  
  prRule _nm _ = ""
  
  prList :: String -> String -> [Rule] -> [String]
  prList dat et rules = concat
      [ if null docs0 then
        [ "if (it.hasNext())" ]
        else
        [ "if (!it.hasNext())"
        , "{ /* nil */"
        , render $ nest 4 $ vcat docs0
        , "}"
        , "else"
        ]
      , if null docs1 then
        [ "{ /* cons */"
        , "  " ++ et ++ " el = it.next();"
        ]
        else
        [ "{"
        , "  " ++ et ++ " el = it.next();"
        , "  if (!it.hasNext())"
        , "  { /* last */"
        , render $ nest 4 $ vcat docs1
        , "  }"
        , "  else"
        , "  { /* cons */"
        ]
      , unlessNull (swRules isConsFun) $ \ docs ->
        [ render $ nest (if null docs1 then 2 else 4) $ vcat docs
        ]
      , unless (null docs1) [ "  }" ]
      , [ "}" ]
      ]
    where
    prules      = sortRulesByPrecedence rules
    swRules f   = switchByPrecedence "_i_" $
                    map (second $ sep . map text . prListRule_ dat) $
                      uniqOn fst $ filter f prules
                      -- Discard duplicates, can only handle one rule per precedence.
    docs0       = swRules isNilFun
    docs1       = swRules isOneFun
  
  -- | Only render the rhs (items) of a list rule.
  
  prListRule_ :: IsFun a => String -> Rul a -> [String]
  prListRule_ dat (Rule _ _ items _) = for items $ \case
    Right t                  -> "render(\"" ++ escapeChars t ++ "\");"
    Left (TokenCat "String") -> "printQuoted(el);"
    Left (ListCat _)         -> "pp" ++ dat ++ "(it, _i_);"
    Left _                   -> "pp(el, _i_);"
  
  -- |
  -- >>> prItem "F" (Right "++")
  --        render("++");
  -- <BLANKLINE>
  -- >>> prItem "F" (Left (TokenCat "String", "string_"))
  --        printQuoted(F.string_);
  -- <BLANKLINE>
  -- >>> prItem "F" (Left (Cat "Abc", "abc_"))
  --        pp(F.abc_, 0);
  -- <BLANKLINE>
  prItem :: Doc -> Either (Cat, Doc) String -> Doc
  prItem _ (Right t) = nest 7 ("render(\"" <> text(escapeChars t) <> "\");\n")
  prItem fnm (Left (TokenCat "String", nt))
      = nest 7 ("printQuoted(" <> fnm <> "." <> nt <> ");\n")
  prItem fnm (Left (cat, nt))
      = nest 7 ("pp(" <> fnm <> "." <> nt <> ", " <> integer (precCat cat) <> ");\n")
  
  --The following methods generate the Show function.
  
  shData :: String -> [UserDef] -> (Cat, [Rule]) -> String
  shData packageAbsyn user (cat, rules) = unlines k
      where
        k = if isList cat
            then
            [ "  private static void sh(" ++ packageAbsyn ++ "."
                  ++ identCat (normCat cat) +++ "foo)"
            , "  {"
            , shList packageAbsyn user cat rules ++ "  }"
            ]
            else
            [ "  private static void sh(" ++ packageAbsyn ++ "."
                  ++ identCat (normCat cat) +++ "foo)"
            , "  {"
            , concatMap (shRule packageAbsyn) rules ++ "  }"
            ]
  
  
  shRule :: String -> Rule -> String
  shRule packageAbsyn (Rule f _c cats _) | not (isCoercion f || isDefinedRule f) = unlines
      [ "    if (foo instanceof" +++ packageAbsyn ++ "." ++ fun ++ ")"
      , "    {"
      , "       " ++ packageAbsyn ++ "." ++ fun +++ fnm +++ "= ("
          ++ packageAbsyn ++ "." ++ fun ++ ") foo;"
      , members ++ "    }"
      ]
    where
      fun = funName f
      members = concat [ lparen
                       , "       render(\"" ++ escapeChars fun ++ "\");\n"
                       , cats'
                       , rparen ]
      cats' = if allTerms cats
          then ""
          else concatMap (render . shCat (text fnm)) (lefts (numVars cats))
      (lparen, rparen) = if allTerms cats
          then ("","")
          else ("       render(\"(\");\n","       render(\")\");\n")
      allTerms [] = True
      allTerms ((Left {}):_) = False
      allTerms (_:zs) = allTerms zs
      fnm = '_' : map toLower fun
  shRule _nm _ = ""
  
  shList :: String -> [UserDef] -> Cat -> [Rule] -> String
  shList packageAbsyn user c _rules = unlines
    [
     "     for (java.util.Iterator<" ++ et
            ++ "> it = foo.iterator(); it.hasNext();)",
     "     {",
     "       sh(it.next());",
     "       if (it.hasNext())",
     "         render(\",\");",
     "     }"
    ]
      where
      et = typename packageAbsyn user $ identCat $ normCatOfList c
  
  -- |
  -- >>> shCat "F" (ListCat (Cat "A"), "lista_")
  --        render("[");
  --        sh(F.lista_);
  --        render("]");
  -- <BLANKLINE>
  -- >>> shCat "F" (Cat "A", "a_")
  --        sh(F.a_);
  -- <BLANKLINE>
  shCat :: Doc -> (Cat, Doc) -> Doc
  shCat fnm (ListCat _, vname) = vcat
      [ "       render(\"[\");"
      , "       sh(" <> fnm <> "." <> vname <> ");"
      , "       render(\"]\");\n" ]
  shCat fname (_, vname)       = "       sh(" <> fname <> "." <> vname <> ");\n"
  
  --Helper function that escapes characters in strings
  escapeChars :: String -> String
  escapeChars [] = []
  escapeChars ('\\':xs) = '\\' : '\\' : escapeChars xs
  escapeChars ('\"':xs) = '\\' : '\"' : escapeChars xs
  escapeChars (x:xs) = x : escapeChars xs
  