
// FIXME writeExcerpt does not work well with the last line in a file

#include <sstream>
#include <cmath>

#include "zen/config.hpp"

#include "bolt/CST.hpp"
#include "bolt/Type.hpp"
#include "bolt/Diagnostics.hpp"

#define ANSI_RESET "\u001b[0m"
#define ANSI_BOLD "\u001b[1m"
#define ANSI_UNDERLINE "\u001b[4m"
#define ANSI_REVERSED "\u001b[7m"

#define ANSI_FG_BLACK "\u001b[30m"
#define ANSI_FG_RED "\u001b[31m"
#define ANSI_FG_GREEN "\u001b[32m"
#define ANSI_FG_YELLOW "\u001b[33m"
#define ANSI_FG_BLUE "\u001b[34m"
#define ANSI_FG_CYAN "\u001b[35m"
#define ANSI_FG_MAGENTA "\u001b[36m"
#define ANSI_FG_WHITE "\u001b[37m"

#define ANSI_BG_BLACK "\u001b[40m"
#define ANSI_BG_RED "\u001b[41m"
#define ANSI_BG_GREEN "\u001b[42m"
#define ANSI_BG_YELLOW "\u001b[43m"
#define ANSI_BG_BLUE "\u001b[44m"
#define ANSI_BG_CYAN "\u001b[45m"
#define ANSI_BG_MAGENTA "\u001b[46m"
#define ANSI_BG_WHITE "\u001b[47m"

namespace bolt {

  template<typename T>
  T countDigits(T number) {
    if (number == 0) {
      return 1;
    }
    return std::ceil(std::log10(number+1));
  }

  Diagnostic::Diagnostic(DiagnosticKind Kind):
    std::runtime_error("a compiler error occurred without being caught"), Kind(Kind) {}

  static std::string describe(NodeKind Type) {
    switch (Type) {
      case NodeKind::Identifier:
        return "an identifier starting with a lowercase letter";
      case NodeKind::IdentifierAlt:
        return "an identifier starting with a capital letter";
      case NodeKind::CustomOperator:
        return "an operator";
      case NodeKind::IntegerLiteral:
        return "an integer literal";
      case NodeKind::EndOfFile:
        return "end-of-file";
      case NodeKind::BlockStart:
        return "the start of a new indented block";
      case NodeKind::BlockEnd:
        return "the end of the current indented block";
      case NodeKind::LineFoldEnd:
        return "the end of the current line-fold";
      case NodeKind::LParen:
        return "'('";
      case NodeKind::RParen:
        return "')'";
      case NodeKind::LBrace:
        return "'['";
      case NodeKind::RBrace:
        return "']'";
      case NodeKind::LBracket:
        return "'{'";
      case NodeKind::RBracket:
        return "'}'";
      case NodeKind::Colon:
        return "':'";
      case NodeKind::Comma:
        return "','";
      case NodeKind::Equals:
        return "'='";
      case NodeKind::StringLiteral:
        return "a string literal";
      case NodeKind::Dot:
        return "'.'";
      case NodeKind::DotDot:
        return "'..'";
      case NodeKind::Tilde:
        return "'~'";
      case NodeKind::RArrow:
        return "'->'";
      case NodeKind::RArrowAlt:
        return "'=>'";
      case NodeKind::PubKeyword:
        return "'pub'";
      case NodeKind::LetKeyword:
        return "'let'";
      case NodeKind::MutKeyword:
        return "'mut'";
      case NodeKind::MatchKeyword:
        return "'match'";
      case NodeKind::ReturnKeyword:
        return "'return'";
      case NodeKind::TypeKeyword:
        return "'type'";
      default:
        ZEN_UNREACHABLE
    }
  }

  std::string describe(const Type* Ty) {
    switch (Ty->getKind()) {
      case TypeKind::Var:
      {
        auto TV = static_cast<const TVar*>(Ty);
        if (TV->getVarKind() == VarKind::Rigid) {
          return static_cast<const TVarRigid*>(TV)->Name;
        }
        return "a" + std::to_string(TV->Id);
      }
      case TypeKind::Arrow:
      {
        auto Y = static_cast<const TArrow*>(Ty);
        std::ostringstream Out;
        Out << "(";
        bool First = true;
        for (auto PT: Y->ParamTypes) {
          if (First) First = false;
          else Out << ", ";
          Out << describe(PT);
        }
        Out << ") -> " << describe(Y->ReturnType);
        return Out.str();
      }
      case TypeKind::Con:
      {
        auto Y = static_cast<const TCon*>(Ty);
        std::ostringstream Out;
        if (!Y->DisplayName.empty()) {
          Out << Y->DisplayName;
        } else {
          Out << "C" << Y->Id;
        }
        for (auto Arg: Y->Args) {
          Out << " " << describe(Arg);
        }
        return Out.str();
      }
      case TypeKind::Tuple:
      {
        std::ostringstream Out;
        auto Y = static_cast<const TTuple*>(Ty);
        Out << "(";
        if (Y->ElementTypes.size()) {
          auto Iter = Y->ElementTypes.begin();
          Out << describe(*Iter++);
          while (Iter != Y->ElementTypes.end()) {
            Out << ", " << describe(*Iter++);
          }
        }
        Out << ")";
        return Out.str();
      }
      case TypeKind::TupleIndex:
      {
        auto Y = static_cast<const TTupleIndex*>(Ty);
        return describe(Y->Ty) + "." + std::to_string(Y->I);
      }
    }
  }

  ConsoleDiagnostics::ConsoleDiagnostics(std::ostream& Out):
    Out(Out) {}

  void ConsoleDiagnostics::setForegroundColor(Color C) {
    if (EnableColors) {
      switch (C) {
        case Color::None:
          break;
        case Color::Black:
          Out << ANSI_FG_BLACK;
          break;
        case Color::White:
          Out << ANSI_FG_WHITE;
          break;
        case Color::Red:
          Out << ANSI_FG_RED;
          break;
        case Color::Yellow:
          Out << ANSI_FG_YELLOW;
          break;
        case Color::Green:
          Out << ANSI_FG_GREEN;
          break;
        case Color::Blue:
          Out << ANSI_FG_BLUE;
          break;
        case Color::Cyan:
          Out << ANSI_FG_CYAN;
          break;
        case Color::Magenta:
          Out << ANSI_FG_MAGENTA;
          break;
      }
    }
  }


  void ConsoleDiagnostics::setBackgroundColor(Color C) {
    if (EnableColors) {
      switch (C) {
        case Color::None:
          break;
        case Color::Black:
          Out << ANSI_BG_BLACK;
          break;
        case Color::White:
          Out << ANSI_BG_WHITE;
          break;
        case Color::Red:
          Out << ANSI_BG_RED;
          break;
        case Color::Yellow:
          Out << ANSI_BG_YELLOW;
          break;
        case Color::Green:
          Out << ANSI_BG_GREEN;
          break;
        case Color::Blue:
          Out << ANSI_BG_BLUE;
          break;
        case Color::Cyan:
          Out << ANSI_BG_CYAN;
          break;
        case Color::Magenta:
          Out << ANSI_BG_MAGENTA;
          break;
      }
    }
  }

  void ConsoleDiagnostics::setBold(bool Enable) {
    if (Enable) {
      Out << ANSI_BOLD;
    }
  }

  void ConsoleDiagnostics::setItalic(bool Enable) {
    if (Enable) {
      // TODO
    }
  }

  void ConsoleDiagnostics::setUnderline(bool Enable) {
    if (Enable) {
      Out << ANSI_UNDERLINE;
    }
  }

  void ConsoleDiagnostics::resetStyles() {
    if (EnableColors) {
      Out << ANSI_RESET;
    }
  }

  void ConsoleDiagnostics::writeGutter(
    std::size_t GutterWidth,
    std::size_t Line
  ) {
    auto LineNumberDigitCount = countDigits(Line);
    auto LeadingSpaces = GutterWidth - LineNumberDigitCount;
    Out << "  ";
    setForegroundColor(Color::Black);
    setBackgroundColor(Color::White);
    for (std::size_t i = 0; i < LeadingSpaces; i++) {
      Out << ' ';
    }
    Out << Line;
    resetStyles();
    Out << " ";
  }

  void ConsoleDiagnostics::writeHighlight(
    std::size_t GutterWidth,
    TextRange Range,
    Color HighlightColor,
    std::size_t Line,
    std::size_t LineLength
  ) {
    if (Line < Range.Start.Line || Range.End.Line < Line) {
      return;
    }
    Out << "  ";
    setBackgroundColor(Color::White);
    for (std::size_t i = 0; i < GutterWidth; i++) {
      Out << ' ';
    }
    resetStyles();
    Out << ' ';
    std::size_t start_column = Range.Start.Line == Line ? Range.Start.Column : 1;
    std::size_t end_column = Range.End.Line == Line ? Range.End.Column : LineLength+1;
    for (std::size_t i = 1; i < start_column; i++) {
      Out << ' ';
    }
    setForegroundColor(HighlightColor);
    for (std::size_t i = start_column; i < end_column; i++) {
      Out << '~';
    }
    resetStyles();
    Out << '\n';
  }

 void ConsoleDiagnostics::writeExcerpt(
    const TextFile& File,
    TextRange ToPrint,
    TextRange ToHighlight,
    Color HighlightColor
  ) {

    auto Text = File.getText();
    auto StartPos = ToPrint.Start;
    auto EndPos = ToPrint.End;
    auto StartLine = StartPos.Line-1 > ExcerptLinesPre ? StartPos.Line - ExcerptLinesPost : 1;
    auto StartOffset = File.getStartOffset(StartLine);
    auto EndLine = std::min(File.getLineCount(), EndPos.Line + ExcerptLinesPost);
    auto EndOffset = File.getStartOffset(EndLine+1);
    auto GutterWidth = std::max<std::size_t>(2, countDigits(EndLine+1));
    auto HighlightStart = ToHighlight.Start;
    auto HighlightEnd = ToHighlight.End;
    auto HighlightRange = TextRange { HighlightStart, HighlightEnd };

    std::size_t CurrColumn = 1;
    std::size_t CurrLine = StartLine;
    writeGutter(GutterWidth, CurrLine);
    for (std::size_t i = StartOffset; i < EndOffset; i++) {
      auto C = Text[i];
      Out << C;
      if (C == '\n') {
        writeHighlight(GutterWidth, HighlightRange, HighlightColor, CurrLine, CurrColumn);
        if (CurrLine == EndLine && C == '\n') {
          break;
        }
        CurrLine++;
        writeGutter(GutterWidth, CurrLine);
        CurrColumn = 1;
      } else {
        CurrColumn++;
      }
    }

  }

  void ConsoleDiagnostics::write(const std::string_view& S) {
    Out << S;
  }

  void ConsoleDiagnostics::write(std::size_t I) {
    Out << I;
  }

  void ConsoleDiagnostics::writeBinding(const ByteString& Name) {
    write("'");
    write(Name);
    write("'");
  }

  void ConsoleDiagnostics::writeType(const Type* Ty) {
    setForegroundColor(Color::Green);
    write(describe(Ty));
    resetStyles();
  }

  void ConsoleDiagnostics::writeType(std::size_t I) {
    setForegroundColor(Color::Green);
    write(I);
    resetStyles();
  }

  void ConsoleDiagnostics::writeNode(const Node* N) {
    auto Range = N->getRange();
    writeExcerpt(N->getSourceFile()->getTextFile(), Range, Range, Color::Red);
  }

  void ConsoleDiagnostics::writeLoc(const TextFile& File, const TextLoc& Loc) {
    setForegroundColor(Color::Yellow);
    write(File.getPath());
    write(":");
    write(Loc.Line);
    write(":");
    write(Loc.Column);
    write(":");
    resetStyles();
  }

  void ConsoleDiagnostics::writePrefix(const Diagnostic& D) {
    setForegroundColor(Color::Red);
    setBold(true);
    write("error: ");
    resetStyles();
  }

  void ConsoleDiagnostics::writeTypeclassName(const ByteString& Name) {
    setForegroundColor(Color::Magenta);
    write(Name);
    resetStyles();
  }

  void ConsoleDiagnostics::writeTypeclassSignature(const TypeclassSignature& Sig) {
    setForegroundColor(Color::Magenta);
    write(Sig.Id);
    for (auto TV: Sig.Params) {
      write(" ");
      write(describe(TV));
    }
    resetStyles();
  }

  void ConsoleDiagnostics::addDiagnostic(Diagnostic* D) {

    switch (D->getKind()) {

      case DiagnosticKind::BindingNotFound:
      {
        auto E = static_cast<const BindingNotFoundDiagnostic&>(*D);
        writePrefix(E);
        write("binding '");
        writeBinding(E.Name);
        write(" was not found\n\n");
        if (E.Initiator != nullptr) {
          auto Range = E.Initiator->getRange();
          //std::cerr << Range.Start.Line << ":" << Range.Start.Column << "-" << Range.End.Line << ":" << Range.End.Column << "\n";
          writeExcerpt(E.Initiator->getSourceFile()->getTextFile(), Range, Range, Color::Red);
          Out << "\n";
        }
        break;
      }

      case DiagnosticKind::UnexpectedToken:
      {
        auto E = static_cast<const UnexpectedTokenDiagnostic&>(*D);
        writePrefix(E);
        writeLoc(E.File, E.Actual->getStartLoc());
        write(" expected ");
        switch (E.Expected.size()) {
          case 0:
            write("nothing");
            break;
          case 1:
            write(describe(E.Expected[0]));
            break;
          default:
            auto Iter = E.Expected.begin();
            Out << describe(*Iter++);
            NodeKind Prev = *Iter++;
            while (Iter != E.Expected.end()) {
              write(", ");
              write(describe(Prev));
              Prev = *Iter++;
            }
            write(" or ");
            write(describe(Prev));
            break;
        }
        write(" but instead got '");
        write(E.Actual->getText());
        write("'\n\n");
        writeExcerpt(E.File, E.Actual->getRange(), E.Actual->getRange(), Color::Red);
        write("\n");
        break;
      }

      case DiagnosticKind::UnexpectedString:
      {
        auto E = static_cast<const UnexpectedStringDiagnostic&>(*D);
        writePrefix(E);
        writeLoc(E.File, E.Location);
        write(" unexpected '");
        for (auto Chr: E.Actual) {
          switch (Chr) {
            case '\\':
              write("\\\\");
              break;
            case '\'':
              write("\\'");
              break;
            default:
              write(Chr);
              break;
          }
        }
        write("'\n\n");
        TextRange Range { E.Location, E.Location + E.Actual };
        writeExcerpt(E.File, Range, Range, Color::Red);
        write("\n");
        break;
      }

      case DiagnosticKind::UnificationError:
      {
        auto E = static_cast<const UnificationErrorDiagnostic&>(*D);
        writePrefix(E);
        auto Left = E.Left->resolve(E.LeftPath);
        auto Right = E.Right->resolve(E.RightPath);
        write("the types ");
        writeType(Left);
        write(" and ");
        writeType(Right);
        write(" failed to match\n\n");
        if (E.Source) {
          writeNode(E.Source);
          Out << "\n";
        }
        if (!E.LeftPath.empty()) {
          setForegroundColor(Color::Yellow);
          setBold(true);
          write("  info: ");
          resetStyles();
          write("the type ");
          writeType(Left);
          write(" occurs in the full type ");
          writeType(E.Left);
          write("\n\n");
        }
        if (!E.RightPath.empty()) {
          setForegroundColor(Color::Yellow);
          setBold(true);
          write("  info: ");
          resetStyles();
          write("the type ");
          writeType(Right);
          write(" occurs in the full type ");
          writeType(E.Right);
          write("\n\n");
        }
        break;
      }

      case DiagnosticKind::TypeclassMissing:
      {
        auto E = static_cast<const TypeclassMissingDiagnostic&>(*D);
        writePrefix(E);
        write("the type class ");
        writeTypeclassSignature(E.Sig);
        write(" is missing from the declaration's type signature\n\n");
        writeNode(E.Decl);
        write("\n\n");
        break;
      }

      case DiagnosticKind::InstanceNotFound:
      {
        auto E = static_cast<const InstanceNotFoundDiagnostic&>(*D);
        writePrefix(E);
        write("a type class instance ");
        writeTypeclassName(E.TypeclassName);
        writeType(E.Ty);
        write(" was not found.\n\n");
        writeNode(E.Source);
        write("\n");
        break;
      }

      case DiagnosticKind::ClassNotFound:
      {
        auto E = static_cast<const ClassNotFoundDiagnostic&>(*D);
        writePrefix(E);
        write("the type class ");
        writeTypeclassName(E.Name);
        write(" was not found.\n\n");
        break;
      }

      case DiagnosticKind::TupleIndexOutOfRange:
      {
        auto E = static_cast<const TupleIndexOutOfRangeDiagnostic&>(*D);
        writePrefix(E);
        write("the index ");
        writeType(E.I);
        write(" is out of range for tuple ");
        writeType(E.Tuple);
        break;
      }

      case DiagnosticKind::InvalidTypeToTypeclass:
      {
        auto E = static_cast<const InvalidTypeToTypeclassDiagnostic&>(*D);
        writePrefix(E);
        write("the type ");
        writeType(E.Actual);
        write(" was applied to type class names ");
        bool First = true;
        for (auto Class: E.Classes) {
          if (First) First = false;
          else write(", ");
          writeTypeclassName(Class);
        }
        write(" but this is invalid\n\n");
        break;
      }

    }

    // Since this DiagnosticEngine is expected to own the diagnostic, we simply
    // destroy the processed diagnostic so that there are no memory leaks.
    delete D;
  }

}
