/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */

#include "mozilla/ArrayUtils.h"

#include "nscore.h"

#include "nsXPLookAndFeel.h"
#include "nsLookAndFeel.h"
#include "nsCRT.h"
#include "nsFont.h"
#include "mozilla/Preferences.h"
#include "mozilla/gfx/2D.h"

#include "gfxPlatform.h"
#include "qcms.h"

#ifdef DEBUG
#include "nsSize.h"
#endif

using namespace mozilla;

nsLookAndFeelIntPref nsXPLookAndFeel::sIntPrefs[] =
{
  { "ui.caretBlinkTime",
    eIntID_CaretBlinkTime,
    false, 0 },
  { "ui.caretWidth",
    eIntID_CaretWidth,
    false, 0 },
  { "ui.caretVisibleWithSelection",
    eIntID_ShowCaretDuringSelection,
    false, 0 },
  { "ui.submenuDelay",
    eIntID_SubmenuDelay,
    false, 0 },
  { "ui.dragThresholdX",
    eIntID_DragThresholdX,
    false, 0 },
  { "ui.dragThresholdY",
    eIntID_DragThresholdY,
    false, 0 },
  { "ui.useAccessibilityTheme",
    eIntID_UseAccessibilityTheme,
    false, 0 },
  { "ui.menusCanOverlapOSBar",
    eIntID_MenusCanOverlapOSBar,
    false, 0 },
  { "ui.useOverlayScrollbars",
    eIntID_UseOverlayScrollbars,
    false, 0 },
  { "ui.scrollbarDisplayOnMouseMove",
    eIntID_ScrollbarDisplayOnMouseMove,
    false, 0 },
  { "ui.scrollbarFadeBeginDelay",
    eIntID_ScrollbarFadeBeginDelay,
    false, 0 },
  { "ui.scrollbarFadeDuration",
    eIntID_ScrollbarFadeDuration,
    false, 0 },
  { "ui.showHideScrollbars",
    eIntID_ShowHideScrollbars,
    false, 0 },
  { "ui.skipNavigatingDisabledMenuItem",
    eIntID_SkipNavigatingDisabledMenuItem,
    false, 0 },
  { "ui.treeOpenDelay",
    eIntID_TreeOpenDelay,
    false, 0 },
  { "ui.treeCloseDelay",
    eIntID_TreeCloseDelay,
    false, 0 },
  { "ui.treeLazyScrollDelay",
    eIntID_TreeLazyScrollDelay,
    false, 0 },
  { "ui.treeScrollDelay",
    eIntID_TreeScrollDelay,
    false, 0 },
  { "ui.treeScrollLinesMax",
    eIntID_TreeScrollLinesMax,
    false, 0 },
  { "accessibility.tabfocus",
    eIntID_TabFocusModel,
    false, 0 },
  { "ui.alertNotificationOrigin",
    eIntID_AlertNotificationOrigin,
    false, 0 },
  { "ui.scrollToClick",
    eIntID_ScrollToClick,
    false, 0 },
  { "ui.IMERawInputUnderlineStyle",
    eIntID_IMERawInputUnderlineStyle,
    false, 0 },
  { "ui.IMESelectedRawTextUnderlineStyle",
    eIntID_IMESelectedRawTextUnderlineStyle,
    false, 0 },
  { "ui.IMEConvertedTextUnderlineStyle",
    eIntID_IMEConvertedTextUnderlineStyle,
    false, 0 },
  { "ui.IMESelectedConvertedTextUnderlineStyle",
    eIntID_IMESelectedConvertedTextUnderline,
    false, 0 },
  { "ui.SpellCheckerUnderlineStyle",
    eIntID_SpellCheckerUnderlineStyle,
    false, 0 },
  { "ui.scrollbarButtonAutoRepeatBehavior",
    eIntID_ScrollbarButtonAutoRepeatBehavior,
    false, 0 },
  { "ui.tooltipDelay",
    eIntID_TooltipDelay,
    false, 0 },
  { "ui.physicalHomeButton",
    eIntID_PhysicalHomeButton,
    false, 0 },
};

nsLookAndFeelFloatPref nsXPLookAndFeel::sFloatPrefs[] =
{
  { "ui.IMEUnderlineRelativeSize",
    eFloatID_IMEUnderlineRelativeSize,
    false, 0 },
  { "ui.SpellCheckerUnderlineRelativeSize",
    eFloatID_SpellCheckerUnderlineRelativeSize,
    false, 0 },
  { "ui.caretAspectRatio",
    eFloatID_CaretAspectRatio,
    false, 0 },
};


// This array MUST be kept in the same order as the color list in LookAndFeel.h.
/* XXX If you add any strings longer than
 * "ui.IMESelectedConvertedTextBackground"
 * to the following array then you MUST update the
 * sizes of the sColorPrefs array in nsXPLookAndFeel.h
 */
const char nsXPLookAndFeel::sColorPrefs[][38] =
{
  "ui.windowBackground",
  "ui.windowForeground",
  "ui.widgetBackground",
  "ui.widgetForeground",
  "ui.widgetSelectBackground",
  "ui.widgetSelectForeground",
  "ui.widget3DHighlight",
  "ui.widget3DShadow",
  "ui.textBackground",
  "ui.textForeground",
  "ui.textSelectBackground",
  "ui.textSelectForeground",
  "ui.textSelectBackgroundDisabled",
  "ui.textSelectBackgroundAttention",
  "ui.textHighlightBackground",
  "ui.textHighlightForeground",
  "ui.IMERawInputBackground",
  "ui.IMERawInputForeground",
  "ui.IMERawInputUnderline",
  "ui.IMESelectedRawTextBackground",
  "ui.IMESelectedRawTextForeground",
  "ui.IMESelectedRawTextUnderline",
  "ui.IMEConvertedTextBackground",
  "ui.IMEConvertedTextForeground",
  "ui.IMEConvertedTextUnderline",
  "ui.IMESelectedConvertedTextBackground",
  "ui.IMESelectedConvertedTextForeground",
  "ui.IMESelectedConvertedTextUnderline",
  "ui.SpellCheckerUnderline",
  "ui.activeborder",
  "ui.activecaption",
  "ui.appworkspace",
  "ui.background",
  "ui.buttonface",
  "ui.buttonhighlight",
  "ui.buttonshadow",
  "ui.buttontext",
  "ui.captiontext",
  "ui.graytext",
  "ui.highlight",
  "ui.highlighttext",
  "ui.inactiveborder",
  "ui.inactivecaption",
  "ui.inactivecaptiontext",
  "ui.infobackground",
  "ui.infotext",
  "ui.menu",
  "ui.menutext",
  "ui.scrollbar",
  "ui.threeddarkshadow",
  "ui.threedface",
  "ui.threedhighlight",
  "ui.threedlightshadow",
  "ui.threedshadow",
  "ui.window",
  "ui.windowframe",
  "ui.windowtext",
  "ui.-moz-buttondefault",
  "ui.-moz-field",
  "ui.-moz-fieldtext",
  "ui.-moz-dialog",
  "ui.-moz-dialogtext",
  "ui.-moz-dragtargetzone",
  "ui.-moz-cellhighlight",
  "ui.-moz_cellhighlighttext",
  "ui.-moz-html-cellhighlight",
  "ui.-moz-html-cellhighlighttext",
  "ui.-moz-buttonhoverface",
  "ui.-moz_buttonhovertext",
  "ui.-moz_menuhover",
  "ui.-moz_menuhovertext",
  "ui.-moz_menubartext",
  "ui.-moz_menubarhovertext",
  "ui.-moz_eventreerow",
  "ui.-moz_oddtreerow",
  "ui.-moz_mac_chrome_active",
  "ui.-moz_mac_chrome_inactive",
  "ui.-moz-mac-focusring",
  "ui.-moz-mac-menuselect",
  "ui.-moz-mac-menushadow",
  "ui.-moz-mac-menutextdisable",
  "ui.-moz-mac-menutextselect",
  "ui.-moz_mac_disabledtoolbartext",
  "ui.-moz-mac-secondaryhighlight",
  "ui.-moz-win-mediatext",
  "ui.-moz-win-communicationstext",
  "ui.-moz-nativehyperlinktext",
  "ui.-moz-comboboxtext",
  "ui.-moz-combobox"
};

int32_t nsXPLookAndFeel::sCachedColors[LookAndFeel::eColorID_LAST_COLOR] = {0};
int32_t nsXPLookAndFeel::sCachedColorBits[COLOR_CACHE_SIZE] = {0};

bool nsXPLookAndFeel::sInitialized = false;
bool nsXPLookAndFeel::sUseNativeColors = true;

nsLookAndFeel* nsXPLookAndFeel::sInstance = nullptr;
bool nsXPLookAndFeel::sShutdown = false;

// static
nsLookAndFeel*
nsXPLookAndFeel::GetInstance()
{
  if (sInstance) {
    return sInstance;
  }

  NS_ENSURE_TRUE(!sShutdown, nullptr);

  sInstance = new nsLookAndFeel();
  return sInstance;
}

// static
void
nsXPLookAndFeel::Shutdown()
{
  if (sShutdown) {
    return;
  }
  sShutdown = true;
  delete sInstance;
  sInstance = nullptr;
}

nsXPLookAndFeel::nsXPLookAndFeel() : LookAndFeel()
{
}

// static
void
nsXPLookAndFeel::IntPrefChanged(nsLookAndFeelIntPref *data)
{
  if (!data) {
    return;
  }

  int32_t intpref;
  nsresult rv = Preferences::GetInt(data->name, &intpref);
  if (NS_FAILED(rv)) {
    return;
  }
  data->intVar = intpref;
  data->isSet = true;
#ifdef DEBUG_akkana
  printf("====== Changed int pref %s to %d\n", data->name, data->intVar);
#endif
}

// static
void
nsXPLookAndFeel::FloatPrefChanged(nsLookAndFeelFloatPref *data)
{
  if (!data) {
    return;
  }

  int32_t intpref;
  nsresult rv = Preferences::GetInt(data->name, &intpref);
  if (NS_FAILED(rv)) {
    return;
  }
  data->floatVar = (float)intpref / 100.0f;
  data->isSet = true;
#ifdef DEBUG_akkana
  printf("====== Changed float pref %s to %f\n", data->name, data->floatVar);
#endif
}

// static
void
nsXPLookAndFeel::ColorPrefChanged (unsigned int index, const char *prefName)
{
  nsAutoString colorStr;
  nsresult rv = Preferences::GetString(prefName, &colorStr);
  if (NS_FAILED(rv)) {
    return;
  }
  if (!colorStr.IsEmpty()) {
    nscolor thecolor;
    if (colorStr[0] == PRUnichar('#')) {
      if (NS_HexToRGB(nsDependentString(colorStr, 1), &thecolor)) {
        int32_t id = NS_PTR_TO_INT32(index);
        CACHE_COLOR(id, thecolor);
      }
    } else if (NS_ColorNameToRGB(colorStr, &thecolor)) {
      int32_t id = NS_PTR_TO_INT32(index);
      CACHE_COLOR(id, thecolor);
#ifdef DEBUG_akkana
      printf("====== Changed color pref %s to 0x%lx\n",
             prefName, thecolor);
#endif
    }
  } else {
    // Reset to the default color, by clearing the cache
    // to force lookup when the color is next used
    int32_t id = NS_PTR_TO_INT32(index);
    CLEAR_COLOR_CACHE(id);
  }
}

void
nsXPLookAndFeel::InitFromPref(nsLookAndFeelIntPref* aPref)
{
  int32_t intpref;
  nsresult rv = Preferences::GetInt(aPref->name, &intpref);
  if (NS_SUCCEEDED(rv)) {
    aPref->isSet = true;
    aPref->intVar = intpref;
  }
}

void
nsXPLookAndFeel::InitFromPref(nsLookAndFeelFloatPref* aPref)
{
  int32_t intpref;
  nsresult rv = Preferences::GetInt(aPref->name, &intpref);
  if (NS_SUCCEEDED(rv)) {
    aPref->isSet = true;
    aPref->floatVar = (float)intpref / 100.0f;
  }
}

void
nsXPLookAndFeel::InitColorFromPref(int32_t i)
{
  nsAutoString colorStr;
  nsresult rv = Preferences::GetString(sColorPrefs[i], &colorStr);
  if (NS_FAILED(rv) || colorStr.IsEmpty()) {
    return;
  }
  nscolor thecolor;
  if (colorStr[0] == PRUnichar('#')) {
    nsAutoString hexString;
    colorStr.Right(hexString, colorStr.Length() - 1);
    if (NS_HexToRGB(hexString, &thecolor)) {
      CACHE_COLOR(i, thecolor);
    }
  } else if (NS_ColorNameToRGB(colorStr, &thecolor)) {
    CACHE_COLOR(i, thecolor);
  }
}

// static
int
nsXPLookAndFeel::OnPrefChanged(const char* aPref, void* aClosure)
{

  // looping in the same order as in ::Init

  nsDependentCString prefName(aPref);
  unsigned int i;
  for (i = 0; i < ArrayLength(sIntPrefs); ++i) {
    if (prefName.Equals(sIntPrefs[i].name)) {
      IntPrefChanged(&sIntPrefs[i]);
      return 0;
    }
  }

  for (i = 0; i < ArrayLength(sFloatPrefs); ++i) {
    if (prefName.Equals(sFloatPrefs[i].name)) {
      FloatPrefChanged(&sFloatPrefs[i]);
      return 0;
    }
  }

  for (i = 0; i < ArrayLength(sColorPrefs); ++i) {
    if (prefName.Equals(sColorPrefs[i])) {
      ColorPrefChanged(i, sColorPrefs[i]);
      return 0;
    }
  }

  return 0;
}

//
// Read values from the user's preferences.
// This is done once at startup, but since the user's preferences
// haven't actually been read yet at that time, we also have to
// set a callback to inform us of changes to each pref.
//
void
nsXPLookAndFeel::Init()
{
  // Say we're already initialized, and take the chance that it might fail;
  // protects against some other process writing to our static variables.
  sInitialized = true;

  // XXX If we could reorganize the pref names, we should separate the branch
  //     for each types.  Then, we could reduce the unnecessary loop from
  //     nsXPLookAndFeel::OnPrefChanged().
  Preferences::RegisterCallback(OnPrefChanged, "ui.");
  Preferences::RegisterCallback(OnPrefChanged, "accessibility.tabfocus");

  unsigned int i;
  for (i = 0; i < ArrayLength(sIntPrefs); ++i) {
    InitFromPref(&sIntPrefs[i]);
  }

  for (i = 0; i < ArrayLength(sFloatPrefs); ++i) {
    InitFromPref(&sFloatPrefs[i]);
  }

  for (i = 0; i < ArrayLength(sColorPrefs); ++i) {
    InitColorFromPref(i);
  }

  bool val;
  if (NS_SUCCEEDED(Preferences::GetBool("ui.use_native_colors", &val))) {
    sUseNativeColors = val;
  }
}

nsXPLookAndFeel::~nsXPLookAndFeel()
{
  NS_ASSERTION(sInstance == this,
               "This destroying instance isn't the singleton instance");
  sInstance = nullptr;
}

bool
nsXPLookAndFeel::IsSpecialColor(ColorID aID, nscolor &aColor)
{
  switch (aID) {
    case eColorID_TextSelectForeground:
      return (aColor == NS_DONT_CHANGE_COLOR);
    case eColorID_IMESelectedRawTextBackground:
    case eColorID_IMESelectedConvertedTextBackground:
    case eColorID_IMERawInputBackground:
    case eColorID_IMEConvertedTextBackground:
    case eColorID_IMESelectedRawTextForeground:
    case eColorID_IMESelectedConvertedTextForeground:
    case eColorID_IMERawInputForeground:
    case eColorID_IMEConvertedTextForeground:
    case eColorID_IMERawInputUnderline:
    case eColorID_IMEConvertedTextUnderline:
    case eColorID_IMESelectedRawTextUnderline:
    case eColorID_IMESelectedConvertedTextUnderline:
    case eColorID_SpellCheckerUnderline:
      return NS_IS_SELECTION_SPECIAL_COLOR(aColor);
    default:
      /*
       * In GetColor(), every color that is not a special color is color
       * corrected. Use false to make other colors color corrected.
       */
      return false;
  }
  return false;
}

//
// All these routines will return NS_OK if they have a value,
// in which case the nsLookAndFeel should use that value;
// otherwise we'll return NS_ERROR_NOT_AVAILABLE, in which case, the
// platform-specific nsLookAndFeel should use its own values instead.
//
nsresult
nsXPLookAndFeel::GetColorImpl(ColorID aID, nscolor &aResult)
{
  if (!sInitialized)
    Init();

  // define DEBUG_SYSTEM_COLOR_USE if you want to debug system color
  // use in a skin that uses them.  When set, it will make all system
  // color pairs that are appropriate for foreground/background
  // pairing the same.  This means if the skin is using system colors
  // correctly you will not be able to see *any* text.
#undef DEBUG_SYSTEM_COLOR_USE

#ifdef DEBUG_SYSTEM_COLOR_USE
  {
    nsresult rv = NS_OK;
    switch (aID) {
        // css2  http://www.w3.org/TR/REC-CSS2/ui.html#system-colors
      case eColorID_activecaption:
          // active window caption background
      case eColorID_captiontext:
          // text in active window caption
        aResult = NS_RGB(0xff, 0x00, 0x00);
        break;

      case eColorID_highlight:
          // background of selected item
      case eColorID_highlighttext:
          // text of selected item
        aResult = NS_RGB(0xff, 0xff, 0x00);
        break;

      case eColorID_inactivecaption:
          // inactive window caption
      case eColorID_inactivecaptiontext:
          // text in inactive window caption
        aResult = NS_RGB(0x66, 0x66, 0x00);
        break;

      case eColorID_infobackground:
          // tooltip background color
      case eColorID_infotext:
          // tooltip text color
        aResult = NS_RGB(0x00, 0xff, 0x00);
        break;

      case eColorID_menu:
          // menu background
      case eColorID_menutext:
          // menu text
        aResult = NS_RGB(0x00, 0xff, 0xff);
        break;

      case eColorID_threedface:
      case eColorID_buttonface:
          // 3-D face color
      case eColorID_buttontext:
          // text on push buttons
        aResult = NS_RGB(0x00, 0x66, 0x66);
        break;

      case eColorID_window:
      case eColorID_windowtext:
        aResult = NS_RGB(0x00, 0x00, 0xff);
        break;

      // from the CSS3 working draft (not yet finalized)
      // http://www.w3.org/tr/2000/wd-css3-userint-20000216.html#color

      case eColorID__moz_field:
      case eColorID__moz_fieldtext:
        aResult = NS_RGB(0xff, 0x00, 0xff);
        break;

      case eColorID__moz_dialog:
      case eColorID__moz_dialogtext:
        aResult = NS_RGB(0x66, 0x00, 0x66);
        break;

      default:
        rv = NS_ERROR_NOT_AVAILABLE;
    }
    if (NS_SUCCEEDED(rv))
      return rv;
  }
#endif // DEBUG_SYSTEM_COLOR_USE

  if (IS_COLOR_CACHED(aID)) {
    aResult = sCachedColors[aID];
    return NS_OK;
  }

  // There are no system color settings for these, so set them manually
  if (aID == eColorID_TextSelectBackgroundDisabled) {
    // This is used to gray out the selection when it's not focused
    // Used with nsISelectionController::SELECTION_DISABLED
    aResult = NS_RGB(0xb0, 0xb0, 0xb0);
    return NS_OK;
  }

  if (aID == eColorID_TextSelectBackgroundAttention) {
    // This makes the selection stand out when typeaheadfind is on
    // Used with nsISelectionController::SELECTION_ATTENTION
    aResult = NS_RGB(0x38, 0xd8, 0x78);
    return NS_OK;
  }

  if (aID == eColorID_TextHighlightBackground) {
    // This makes the matched text stand out when findbar highlighting is on
    // Used with nsISelectionController::SELECTION_FIND
    aResult = NS_RGB(0xef, 0x0f, 0xff);
    return NS_OK;
  }

  if (aID == eColorID_TextHighlightForeground) {
    // The foreground color for the matched text in findbar highlighting
    // Used with nsISelectionController::SELECTION_FIND
    aResult = NS_RGB(0xff, 0xff, 0xff);
    return NS_OK;
  }

  if (sUseNativeColors && NS_SUCCEEDED(NativeGetColor(aID, aResult))) {
    if ((gfxPlatform::GetCMSMode() == eCMSMode_All) &&
         !IsSpecialColor(aID, aResult)) {
      qcms_transform *transform = gfxPlatform::GetCMSInverseRGBTransform();
      if (transform) {
        uint8_t color[3];
        color[0] = NS_GET_R(aResult);
        color[1] = NS_GET_G(aResult);
        color[2] = NS_GET_B(aResult);
        qcms_transform_data(transform, color, color, 1);
        aResult = NS_RGB(color[0], color[1], color[2]);
      }
    }

    CACHE_COLOR(aID, aResult);
    return NS_OK;
  }

  return NS_ERROR_NOT_AVAILABLE;
}
  
nsresult
nsXPLookAndFeel::GetIntImpl(IntID aID, int32_t &aResult)
{
  if (!sInitialized)
    Init();

  // Set the default values for these prefs. but allow different platforms
  // to override them in their nsLookAndFeel if desired.
  switch (aID) {
    case eIntID_ScrollButtonLeftMouseButtonAction:
      aResult = 0;
      return NS_OK;
    case eIntID_ScrollButtonMiddleMouseButtonAction:
      aResult = 3;
      return NS_OK;
    case eIntID_ScrollButtonRightMouseButtonAction:
      aResult = 3;
      return NS_OK;
    default:
      /*
       * The metrics above are hardcoded platform defaults. All the other
       * metrics are stored in sIntPrefs and can be changed at runtime.
       */
    break;
  }

  for (unsigned int i = 0; i < ArrayLength(sIntPrefs); ++i) {
    if (sIntPrefs[i].isSet && (sIntPrefs[i].id == aID)) {
      aResult = sIntPrefs[i].intVar;
      return NS_OK;
    }
  }

  return NS_ERROR_NOT_AVAILABLE;
}

nsresult
nsXPLookAndFeel::GetFloatImpl(FloatID aID, float &aResult)
{
  if (!sInitialized)
    Init();

  for (unsigned int i = 0; i < ArrayLength(sFloatPrefs); ++i) {
    if (sFloatPrefs[i].isSet && sFloatPrefs[i].id == aID) {
      aResult = sFloatPrefs[i].floatVar;
      return NS_OK;
    }
  }

  return NS_ERROR_NOT_AVAILABLE;
}

void
nsXPLookAndFeel::RefreshImpl()
{
  // Wipe out our color cache.
  uint32_t i;
  for (i = 0; i < eColorID_LAST_COLOR; i++)
    sCachedColors[i] = 0;
  for (i = 0; i < COLOR_CACHE_SIZE; i++)
    sCachedColorBits[i] = 0;
}

namespace mozilla {

// static
nsresult
LookAndFeel::GetColor(ColorID aID, nscolor* aResult)
{
  return nsLookAndFeel::GetInstance()->GetColorImpl(aID, *aResult);
}

// static
nsresult
LookAndFeel::GetInt(IntID aID, int32_t* aResult)
{
  return nsLookAndFeel::GetInstance()->GetIntImpl(aID, *aResult);
}

// static
nsresult
LookAndFeel::GetFloat(FloatID aID, float* aResult)
{
  return nsLookAndFeel::GetInstance()->GetFloatImpl(aID, *aResult);
}

// static
bool
LookAndFeel::GetFont(FontID aID, nsString& aName, gfxFontStyle& aStyle,
                     float aDevPixPerCSSPixel)
{
  return nsLookAndFeel::GetInstance()->GetFontImpl(aID, aName, aStyle,
                                                   aDevPixPerCSSPixel);
}

// static
PRUnichar
LookAndFeel::GetPasswordCharacter()
{
  return nsLookAndFeel::GetInstance()->GetPasswordCharacterImpl();
}

// static
bool
LookAndFeel::GetEchoPassword()
{
  return nsLookAndFeel::GetInstance()->GetEchoPasswordImpl();
}

// static
uint32_t
LookAndFeel::GetPasswordMaskDelay()
{
  return nsLookAndFeel::GetInstance()->GetPasswordMaskDelayImpl();
}

// static
void
LookAndFeel::Refresh()
{
  nsLookAndFeel::GetInstance()->RefreshImpl();
}

} // namespace mozilla
