// Copyright 2005-2024 Google LLC
//
// Licensed 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
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an 'AS IS' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// See www.openfst.org for extensive documentation on this weighted
// finite-state transducer library.
//
// Class to draw a binary FST by producing a text file in dot format, a helper
// class to fstdraw.cc.

#ifndef FST_SCRIPT_DRAW_IMPL_H_
#define FST_SCRIPT_DRAW_IMPL_H_

#include <iomanip>
#include <ios>
#include <ostream>
#include <sstream>
#include <string>

#include <string_view>
#include <fst/fst.h>
#include <fst/properties.h>
#include <fst/symbol-table.h>
#include <fst/util.h>

namespace fst {

// Print a binary FST in GraphViz or D2 textual formats (helper class for
// fstdraw.cc).
// WARNING: Stand-alone use not recommend.
template <class Arc>
class FstDrawer {
 public:
  using Label = typename Arc::Label;
  using StateId = typename Arc::StateId;
  using Weight = typename Arc::Weight;

  FstDrawer(const Fst<Arc>& fst, const SymbolTable* isyms,
            const SymbolTable* osyms, const SymbolTable* ssyms, bool accep,
            std::string_view title, float width, float height, bool portrait,
            bool vertical, float ranksep, float nodesep, int fontsize,
            int precision, std::string_view float_format, bool show_weight_one,
            std::string_view format)
      : fst_(fst),
        isyms_(isyms),
        osyms_(osyms),
        ssyms_(ssyms),
        accep_(accep && fst.Properties(kAcceptor, true)),
        title_(title),
        width_(width),
        height_(height),
        portrait_(portrait),
        vertical_(vertical),
        ranksep_(ranksep),
        nodesep_(nodesep),
        fontsize_(fontsize),
        precision_(precision),
        float_format_(float_format),
        show_weight_one_(show_weight_one),
        format_(format) {}

  // Draws FST to an output buffer.
  void Draw(std::ostream& strm, std::string_view dest) {
    if (format_ == "dot") {
      DrawDot(strm, dest);
    } else if (format_ == "d2") {
      DrawD2(strm, dest);
    } else {
      FSTERROR() << "FstDrawer: Unknown format: " << format_;
    }
  }

 private:
  void DrawDot(std::ostream& strm, std::string_view dest) {
    SetStreamState(strm);
    dest_ = std::string(dest);
    const auto start = fst_.Start();
    if (start == kNoStateId) return;
    strm << "digraph FST {\n";
    if (vertical_) {
      strm << "rankdir = BT;\n";
    } else {
      strm << "rankdir = LR;\n";
    }
    strm << "size = \"" << width_ << "," << height_ << "\";\n";
    if (!title_.empty()) strm << "label = \"" + title_ + "\";\n";
    strm << "center = 1;\n";
    if (portrait_) {
      strm << "orientation = Portrait;\n";
    } else {
      strm << "orientation = Landscape;\n";
    }
    strm << "ranksep = \"" << ranksep_ << "\";\n"
         << "nodesep = \"" << nodesep_ << "\";\n";
    // Initial state first.
    DrawDotState(strm, start);
    for (StateIterator<Fst<Arc>> siter(fst_); !siter.Done(); siter.Next()) {
      const auto s = siter.Value();
      if (s != start) DrawDotState(strm, s);
    }
    strm << "}\n";
  }

  void DrawD2(std::ostream& strm, std::string_view dest) {
    SetStreamState(strm);
    dest_ = std::string(dest);
    const auto start = fst_.Start();
    if (start == kNoStateId) return;
    if (vertical_) {
      strm << "direction: up\n";
    } else {
      strm << "direction: right\n";
    }
    if (!title_.empty()) strm << "title: \"" << title_ << "\"\n";
    // All shapes are circles.
    strm << "*: {\n"
         << "  shape: circle\n"
         << "}\n";
    // Initial state first.
    DrawD2State(strm, start);
    for (StateIterator<Fst<Arc>> siter(fst_); !siter.Done(); siter.Next()) {
      const auto s = siter.Value();
      if (s != start) DrawD2State(strm, s);
    }
  }

  void SetStreamState(std::ostream& strm) const {
    strm << std::setprecision(precision_);
    if (float_format_ == "e") strm << std::scientific;
    if (float_format_ == "f") strm << std::fixed;
    // O.w. defaults to "g" per standard lib.
  }

  // Escapes backslash and double quote if these occur in the string. Dot
  // will not deal gracefully with these if they are not escaped.
  static std::string Escape(std::string_view str) {
    std::string ns;
    for (char c : str) {
      if (c == '\\' || c == '"') ns.push_back('\\');
      ns.push_back(c);
    }
    return ns;
  }

  std::string FormatId(StateId id, const SymbolTable* syms) const {
    if (syms) {
      auto symbol = syms->Find(id);
      if (symbol.empty()) {
        FSTERROR() << "FstDrawer: Integer " << id
                   << " is not mapped to any textual symbol"
                   << ", symbol table = " << syms->Name()
                   << ", destination = " << dest_;
        symbol = "?";
      }
      return Escape(symbol);
    } else {
      return std::to_string(id);
    }
  }

  std::string FormatStateId(StateId s) const { return FormatId(s, ssyms_); }

  std::string FormatILabel(Label label) const {
    return FormatId(label, isyms_);
  }

  std::string FormatOLabel(Label label) const {
    return FormatId(label, osyms_);
  }

  std::string FormatWeight(Weight w) const {
    std::stringstream ss;
    SetStreamState(ss);
    ss << w;
    // Weight may have double quote characters in it, so escape it.
    return Escape(ss.str());
  }

  void DrawDotState(std::ostream& strm, StateId s) const {
    strm << s << " [label = \"" << FormatStateId(s);
    const auto weight = fst_.Final(s);
    if (weight != Weight::Zero()) {
      if (show_weight_one_ || (weight != Weight::One())) {
        strm << "/" << FormatWeight(weight);
      }
      strm << "\", shape = doublecircle,";
    } else {
      strm << "\", shape = circle,";
    }
    if (s == fst_.Start()) {
      strm << " style = bold,";
    } else {
      strm << " style = solid,";
    }
    strm << " fontsize = " << fontsize_ << "]\n";
    for (ArcIterator<Fst<Arc>> aiter(fst_, s); !aiter.Done(); aiter.Next()) {
      const auto& arc = aiter.Value();
      strm << "\t" << s << " -> " << arc.nextstate << " [label = \""
           << FormatILabel(arc.ilabel);
      if (!accep_) {
        strm << ":" << FormatOLabel(arc.olabel);
      }
      if (show_weight_one_ || (arc.weight != Weight::One())) {
        strm << "/" << FormatWeight(arc.weight);
      }
      strm << "\", fontsize = " << fontsize_ << "];\n";
    }
  }

  void DrawD2State(std::ostream& strm, StateId s) const {
    strm << s << ": {\n";
    strm << "  label: \"" << FormatStateId(s);
    const auto weight = fst_.Final(s);
    if (weight != Weight::Zero()) {
      if (show_weight_one_ || (weight != Weight::One())) {
        strm << "/" << FormatWeight(weight);
      }
      strm << "\"\n";
      strm << "  style.double-border: true\n";
    } else {
      strm << "\"\n";
    }
    if (s == fst_.Start()) {
      strm << "  style.bold: true\n";
    }
    strm << "}\n";
    for (ArcIterator<Fst<Arc>> aiter(fst_, s); !aiter.Done(); aiter.Next()) {
      const auto& arc = aiter.Value();
      strm << s << " -> " << arc.nextstate << ": \""
           << FormatILabel(arc.ilabel);
      if (!accep_) {
        strm << ":" << FormatOLabel(arc.olabel);
      }
      if (show_weight_one_ || (arc.weight != Weight::One())) {
        strm << "/" << FormatWeight(arc.weight);
      }
      strm << "\"\n";
    }
  }

  const Fst<Arc>& fst_;
  const SymbolTable* isyms_;  // ilabel symbol table.
  const SymbolTable* osyms_;  // olabel symbol table.
  const SymbolTable* ssyms_;  // slabel symbol table.
  bool accep_;                // Print as acceptor when possible.
  std::string dest_;          // Drawn FST destination name.

  std::string title_;
  float width_;
  float height_;
  bool portrait_;
  bool vertical_;
  float ranksep_;
  float nodesep_;
  int fontsize_;
  int precision_;
  std::string float_format_;
  bool show_weight_one_;
  std::string format_;

  FstDrawer(const FstDrawer&) = delete;
  FstDrawer& operator=(const FstDrawer&) = delete;
};

}  // namespace fst

#endif  // FST_SCRIPT_DRAW_IMPL_H_
