// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/diagnostics/diagnostics_main.h"

#if defined(OS_POSIX)
#include <stdio.h>
#include <unistd.h>
#endif

#include <iostream>

#include "app/app_paths.h"
#include "base/basictypes.h"
#include "base/command_line.h"
#include "base/i18n/icu_util.h"
#include "base/string_util.h"
#include "base/sys_string_conversions.h"
#include "base/time.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/diagnostics/diagnostics_model.h"
#include "chrome/common/chrome_paths.h"
#include "ui/base/ui_base_paths.h"

namespace {
// This is a minimalistic interface to wrap the platform console.  This will be
// eventually replaced by a view that can be subclassed for each platform and
// that the approved look and feel.
class SimpleConsole {
 public:
  enum Color {
    DEFAULT,
    RED,
    GREEN,
  };

  virtual ~SimpleConsole() { }

  // Init must be called before using any other method. If it returns
  // false there would be no console output.
  virtual bool Init() = 0;

  // Writes a string to the console with the current color.
  virtual bool Write(const std::wstring& text) = 0;

  // Reads a string from the console. Internally it may be limited to 256
  // characters.
  virtual bool Read(std::wstring* txt) = 0;

  // Sets the foreground and background color.
  virtual bool SetColor(Color color) = 0;

  // Create an appropriate SimpleConsole instance.  May return NULL if there is
  // no implementation for the current platform.
  static SimpleConsole* Create();
};

#if defined(OS_WIN)
// Wrapper for the windows console operating in high-level IO mode.
class WinConsole : public SimpleConsole {
 public:
  // The ctor allocates a console always. This avoids having to ask
  // the user to start chrome from a command prompt.
  WinConsole()
      : std_out_(INVALID_HANDLE_VALUE),
        std_in_(INVALID_HANDLE_VALUE)  {
  }

  virtual ~WinConsole() {
    ::FreeConsole();
  }

  virtual bool Init() {
    ::AllocConsole();
    return SetIOHandles();
  }

  virtual bool Write(const std::wstring& txt) {
    DWORD sz = txt.size();
    return (TRUE == ::WriteConsoleW(std_out_, txt.c_str(), sz, &sz, NULL));
  }

  // Reads a string from the console. Internally it is limited to 256
  // characters.
  virtual bool Read(std::wstring* txt) {
    wchar_t buf[256];
    DWORD read = sizeof(buf) - sizeof(buf[0]);
    if (!::ReadConsoleW(std_in_, buf, read, &read, NULL))
      return false;
    // Note that |read| is in bytes.
    txt->assign(buf, read/2);
    return true;
  }

  // Sets the foreground and background color.
  virtual bool SetColor(Color color) {
    uint16 color_combo =
        FOREGROUND_RED|FOREGROUND_GREEN|FOREGROUND_BLUE|FOREGROUND_INTENSITY;
    switch (color) {
      case RED:
        color_combo = FOREGROUND_RED|FOREGROUND_INTENSITY;
        break;
      case GREEN:
        color_combo = FOREGROUND_GREEN|FOREGROUND_INTENSITY;
        break;
      case DEFAULT:
        break;
      default:
        NOTREACHED();
    }
    return (TRUE == ::SetConsoleTextAttribute(std_out_, color_combo));
  }

 private:
  bool SetIOHandles() {
    std_out_ = ::GetStdHandle(STD_OUTPUT_HANDLE);
    std_in_ = ::GetStdHandle(STD_INPUT_HANDLE);
    return ((std_out_ != INVALID_HANDLE_VALUE) &&
            (std_in_ != INVALID_HANDLE_VALUE));
  }

  // The input and output handles to the screen. They seem to be
  // implemented as pipes but they have non-documented protocol.
  HANDLE std_out_;
  HANDLE std_in_;

  DISALLOW_COPY_AND_ASSIGN(WinConsole);
};

SimpleConsole* SimpleConsole::Create() {
  return new WinConsole();
}

#elif defined(OS_POSIX)

class PosixConsole : public SimpleConsole {
 public:
  PosixConsole() : use_color_(false) { }

  virtual bool Init() {
    // Technically, we should also check the terminal capabilities before using
    // color, but in practice this is unlikely to be an issue.
    use_color_ = isatty(STDOUT_FILENO);
    return true;
  }

  virtual bool Write(const std::wstring& text) {
    printf("%s", base::SysWideToNativeMB(text).c_str());
    return true;
  }

  virtual bool Read(std::wstring* txt) {
    std::string input;
    if (!std::getline(std::cin, input)) {
      std::cin.clear();
      return false;
    }
    *txt = UTF8ToWide(input);
    return true;
  }

  virtual bool SetColor(Color color) {
    if (!use_color_)
      return false;

    const char* code = "\033[m";
    switch (color) {
      case RED:
        code = "\033[1;31m";
        break;
      case GREEN:
        code = "\033[1;32m";
        break;
      case DEFAULT:
        break;
      default:
        NOTREACHED();
    }
    printf("%s", code);
    return true;
  }

 private:
  bool use_color_;

  DISALLOW_COPY_AND_ASSIGN(PosixConsole);
};

SimpleConsole* SimpleConsole::Create() {
  return new PosixConsole();
}

#else  // !defined(OS_WIN) && !defined(OS_POSIX)

SimpleConsole* SimpleConsole::Create() {
  return NULL;
}
#endif

// This class wraps a SimpleConsole for the specific use case of
// writing the results of the diagnostic tests.
// TODO(cpu) figure out the localization strategy.
class TestWriter {
 public:
  // The |console| must be valid and properly initialized. This
  // class does not own it.
  explicit TestWriter(SimpleConsole* console)
      : console_(console),
        failures_(0) {
  }

  // How many tests reported failure.
  int failures() { return failures_; }

  // Write an informational line of text in white over black.
  bool WriteInfoText(const std::wstring& txt) {
    console_->SetColor(SimpleConsole::DEFAULT);
    return console_->Write(txt);
  }

  // Write a result block. It consist of two lines. The first line
  // has [PASS] or [FAIL] with |name| and the second line has
  // the text in |extra|.
  bool WriteResult(bool success, const std::wstring& name,
                   const std::wstring& extra) {
    if (success) {
      console_->SetColor(SimpleConsole::GREEN);
      console_->Write(L"[PASS] ");
    } else {
      console_->SetColor(SimpleConsole::RED);
      console_->Write(L"[FAIL] ");
      failures_++;
    }
    WriteInfoText(name + L"\n");
    std::wstring second_line(L"   ");
    second_line.append(extra);
    return WriteInfoText(second_line + L"\n\n");
  }

 private:

  SimpleConsole* console_;

  // Keeps track of how many tests reported failure.
  int failures_;

  DISALLOW_COPY_AND_ASSIGN(TestWriter);
};

std::wstring PrintableUSCurrentTime() {
  base::Time::Exploded exploded = {0};
  base::Time::Now().UTCExplode(&exploded);
  return StringPrintf(L"%d:%d:%d.%d:%d:%d",
      exploded.year, exploded.month, exploded.day_of_month,
      exploded.hour, exploded.minute, exploded.second);
}

// This class is a basic test controller. In this design the view (TestWriter)
// and the model (DiagnosticsModel) do not talk to each other directly but they
// are mediated by the controller. This has a name: 'passive view'.
// More info at http://martinfowler.com/eaaDev/PassiveScreen.html
class TestController : public DiagnosticsModel::Observer {
 public:
  explicit TestController(TestWriter* writer)
      : model_(NULL),
        writer_(writer) {
  }

  // Run all the diagnostics of |model| and invoke the view as the model
  // callbacks arrive.
  void Run(DiagnosticsModel* model) {
    std::wstring title(L"Chrome Diagnostics Mode (");
    writer_->WriteInfoText(title.append(PrintableUSCurrentTime()) + L")\n");
    if (!model) {
      writer_->WriteResult(false, L"Diagnostics start", L"model is null");
      return;
    }
    bool icu_result = icu_util::Initialize();
    if (!icu_result) {
      writer_->WriteResult(false, L"Diagnostics start", L"ICU failure");
      return;
    }
    int count = model->GetTestAvailableCount();
    writer_->WriteInfoText(StringPrintf(L"%d available test(s)\n\n", count));
    model->RunAll(this);
  }

  // Next four are overridden from DiagnosticsModel::Observer.
  virtual void OnProgress(int id, int percent, DiagnosticsModel* model) {
  }

  virtual void OnSkipped(int id, DiagnosticsModel* model) {
    // TODO(cpu): display skipped tests.
  }

  virtual void OnFinished(int id, DiagnosticsModel* model) {
    // As each test completes we output the results.
    ShowResult(model->GetTest(id));
  }

  virtual void OnDoneAll(DiagnosticsModel* model) {
    if (writer_->failures() > 0) {
      writer_->WriteInfoText(StringPrintf(L"DONE. %d failure(s)\n\n",
                             writer_->failures()));
    } else {
      writer_->WriteInfoText(L"DONE\n\n");
    }
  }

 private:
  void ShowResult(DiagnosticsModel::TestInfo& test_info) {
    bool success = (DiagnosticsModel::TEST_OK == test_info.GetResult());
    writer_->WriteResult(success, UTF16ToWide(test_info.GetTitle()),
                         UTF16ToWide(test_info.GetAdditionalInfo()));
  }

  DiagnosticsModel* model_;
  TestWriter* writer_;

  DISALLOW_COPY_AND_ASSIGN(TestController);
};
}  // namespace

// This entry point is called from ChromeMain() when very few things
// have been initialized. To wit:
// -(win)   Breakpad
// -(macOS) base::EnableTerminationOnHeapCorruption()
// -(macOS) base::EnableTerminationOnOutOfMemory()
// -(all)   RegisterInvalidParamHandler()
// -(all)   base::AtExitManager::AtExitManager()
// -(macOS) base::ScopedNSAutoreleasePool
// -(posix) base::GlobalDescriptors::GetInstance()->Set(kPrimaryIPCChannel)
// -(linux) base::GlobalDescriptors::GetInstance()->Set(kCrashDumpSignal)
// -(posix) setlocale(LC_ALL,..)
// -(all)   CommandLine::Init();

int DiagnosticsMain(const CommandLine& command_line) {
  // If we can't initialize the console exit right away.
  SimpleConsole* console = SimpleConsole::Create();
  if (!console || !console->Init())
    return 1;

  // We need to have the path providers registered. They both
  // return void so there is no early error signal that we can use.
  app::RegisterPathProvider();
  ui::RegisterPathProvider();
  chrome::RegisterPathProvider();

  TestWriter writer(console);
  DiagnosticsModel* model = MakeDiagnosticsModel(command_line);
  TestController controller(&writer);

  // Run all the diagnostic tests.
  controller.Run(model);
  delete model;

  // The "press enter to continue" prompt isn't very unixy, so only do that on
  // Windows.
#if defined(OS_WIN)
  // Block here so the user can see the results.
  writer.WriteInfoText(L"Press [enter] to continue\n");
  std::wstring txt;
  console->Read(&txt);
#endif
  delete console;
  return 0;
}