/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=2 ts=8 et ft=cpp : */
/* Copyright 2012 Mozilla Foundation and Mozilla contributors
 *
 * 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.
 */

#include "base/basictypes.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/Hal.h"
#include "nsIScreen.h"
#include "nsIScreenManager.h"
#include "OrientationObserver.h"
#include "mozilla/HalSensor.h"
#include "ProcessOrientation.h"
#include "nsServiceManagerUtils.h"

using namespace mozilla;
using namespace dom;

namespace {

struct OrientationMapping {
  uint32_t mScreenRotation;
  ScreenOrientationInternal mDomOrientation;
};

static OrientationMapping sOrientationMappings[] = {
  {nsIScreen::ROTATION_0_DEG,   eScreenOrientation_PortraitPrimary},
  {nsIScreen::ROTATION_180_DEG, eScreenOrientation_PortraitSecondary},
  {nsIScreen::ROTATION_90_DEG,  eScreenOrientation_LandscapePrimary},
  {nsIScreen::ROTATION_270_DEG, eScreenOrientation_LandscapeSecondary},
};

const static uint32_t sDefaultLandscape = 2;
const static uint32_t sDefaultPortrait = 0;

static uint32_t sOrientationOffset = 0;

static already_AddRefed<nsIScreen>
GetPrimaryScreen()
{
  nsCOMPtr<nsIScreenManager> screenMgr =
    do_GetService("@mozilla.org/gfx/screenmanager;1");
  NS_ENSURE_TRUE(screenMgr, nullptr);

  nsCOMPtr<nsIScreen> screen;
  screenMgr->GetPrimaryScreen(getter_AddRefs(screen));
  return screen.forget();
}

static void
DetectDefaultOrientation()
{
  nsCOMPtr<nsIScreen> screen = GetPrimaryScreen();
  if (!screen) {
    return;
  }

  int32_t left, top, width, height;
  if (NS_FAILED(screen->GetRect(&left, &top, &width, &height))) {
    return;
  }

  uint32_t rotation;
  if (NS_FAILED(screen->GetRotation(&rotation))) {
    return;
  }

  if (width < height) {
    if (rotation == nsIScreen::ROTATION_0_DEG ||
        rotation == nsIScreen::ROTATION_180_DEG) {
      sOrientationOffset = sDefaultPortrait;
    } else {
      sOrientationOffset = sDefaultLandscape;
    }
  } else {
    if (rotation == nsIScreen::ROTATION_0_DEG ||
        rotation == nsIScreen::ROTATION_180_DEG) {
      sOrientationOffset = sDefaultLandscape;
    } else {
      sOrientationOffset = sDefaultPortrait;
    }
  }
}

/**
 * Converts DOM orientation to nsIScreen rotation. Portrait and Landscape are
 * treated as PortraitPrimary and LandscapePrimary, respectively, during
 * conversion.
 *
 * @param aOrientation DOM orientation e.g.
 *        dom::eScreenOrientation_PortraitPrimary.
 * @param aResult output nsIScreen rotation e.g. nsIScreen::ROTATION_0_DEG.
 * @return NS_OK on success. NS_ILLEGAL_VALUE on failure.
 */
static nsresult
ConvertToScreenRotation(ScreenOrientationInternal aOrientation, uint32_t *aResult)
{
  for (uint32_t i = 0; i < ArrayLength(sOrientationMappings); i++) {
    if (aOrientation & sOrientationMappings[i].mDomOrientation) {
      // Shift the mappings in sOrientationMappings so devices with default
      // landscape orientation map landscape-primary to 0 degree and so forth.
      int adjusted = (i + sOrientationOffset) %
                     ArrayLength(sOrientationMappings);
      *aResult = sOrientationMappings[adjusted].mScreenRotation;
      return NS_OK;
    }
  }

  *aResult = nsIScreen::ROTATION_0_DEG;
  return NS_ERROR_ILLEGAL_VALUE;
}

/**
 * Converts nsIScreen rotation to DOM orientation.
 *
 * @param aRotation nsIScreen rotation e.g. nsIScreen::ROTATION_0_DEG.
 * @param aResult output DOM orientation e.g.
 *        dom::eScreenOrientation_PortraitPrimary.
 * @return NS_OK on success. NS_ILLEGAL_VALUE on failure.
 */
nsresult
ConvertToDomOrientation(uint32_t aRotation, ScreenOrientationInternal *aResult)
{
  for (uint32_t i = 0; i < ArrayLength(sOrientationMappings); i++) {
    if (aRotation == sOrientationMappings[i].mScreenRotation) {
      // Shift the mappings in sOrientationMappings so devices with default
      // landscape orientation map 0 degree to landscape-primary and so forth.
      int adjusted = (i + sOrientationOffset) %
                     ArrayLength(sOrientationMappings);
      *aResult = sOrientationMappings[adjusted].mDomOrientation;
      return NS_OK;
    }
  }

  *aResult = eScreenOrientation_None;
  return NS_ERROR_ILLEGAL_VALUE;
}

// Note that all operations with sOrientationSensorObserver
// should be on the main thread.
static StaticAutoPtr<OrientationObserver> sOrientationSensorObserver;

} // namespace

OrientationObserver*
OrientationObserver::GetInstance()
{
  if (!sOrientationSensorObserver) {
    sOrientationSensorObserver = new OrientationObserver();
    ClearOnShutdown(&sOrientationSensorObserver);
  }

  return sOrientationSensorObserver;
}

OrientationObserver::OrientationObserver()
  : mAutoOrientationEnabled(false)
  , mAllowedOrientations(sDefaultOrientations)
  , mOrientation(new mozilla::ProcessOrientation())
{
  DetectDefaultOrientation();

  EnableAutoOrientation();
}

OrientationObserver::~OrientationObserver()
{
  if (mAutoOrientationEnabled) {
    DisableAutoOrientation();
  }
}

/* static */ void
OrientationObserver::ShutDown()
{
  if (!sOrientationSensorObserver) {
    return;
  }

  if (sOrientationSensorObserver->mAutoOrientationEnabled) {
    sOrientationSensorObserver->DisableAutoOrientation();
  }
}

void
OrientationObserver::Notify(const hal::SensorData& aSensorData)
{
  // Sensor will call us on the main thread.
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aSensorData.sensor() == hal::SensorType::SENSOR_ACCELERATION);

  nsCOMPtr<nsIScreen> screen = GetPrimaryScreen();
  if (!screen) {
    return;
  }

  uint32_t currRotation;
  if(NS_FAILED(screen->GetRotation(&currRotation))) {
    return;
  }

  int rotation = mOrientation->OnSensorChanged(aSensorData, static_cast<int>(currRotation));
  if (rotation < 0 || uint32_t(rotation) == currRotation) {
    return;
  }

  ScreenOrientationInternal orientation;
  if (NS_FAILED(ConvertToDomOrientation(rotation, &orientation))) {
    return;
  }

  if ((mAllowedOrientations & orientation) == eScreenOrientation_None) {
    // The orientation from sensor is not allowed.
    return;
  }

  if (NS_FAILED(screen->SetRotation(static_cast<uint32_t>(rotation)))) {
    // Don't notify dom on rotation failure.
    return;
  }
}

/**
 * Register the observer. Note that the observer shouldn't be registered.
 */
void
OrientationObserver::EnableAutoOrientation()
{
  MOZ_ASSERT(NS_IsMainThread() && !mAutoOrientationEnabled);

  mOrientation->Reset();
  hal::RegisterSensorObserver(hal::SENSOR_ACCELERATION, this);
  mAutoOrientationEnabled = true;
}

/**
 * Unregister the observer. Note that the observer should already be registered.
 */
void
OrientationObserver::DisableAutoOrientation()
{
  MOZ_ASSERT(NS_IsMainThread() && mAutoOrientationEnabled);

  hal::UnregisterSensorObserver(hal::SENSOR_ACCELERATION, this);
  mAutoOrientationEnabled = false;
}

bool
OrientationObserver::LockScreenOrientation(ScreenOrientationInternal aOrientation)
{
  MOZ_ASSERT(aOrientation | (eScreenOrientation_PortraitPrimary |
                             eScreenOrientation_PortraitSecondary |
                             eScreenOrientation_LandscapePrimary |
                             eScreenOrientation_LandscapeSecondary |
                             eScreenOrientation_Default));

  if (aOrientation == eScreenOrientation_Default) {
    aOrientation = (sOrientationOffset == sDefaultPortrait) ?
                    eScreenOrientation_PortraitPrimary :
                    eScreenOrientation_LandscapePrimary;
  }

  // If there are multiple orientations allowed, we should enable the
  // auto-rotation.
  if (aOrientation != eScreenOrientation_LandscapePrimary &&
      aOrientation != eScreenOrientation_LandscapeSecondary &&
      aOrientation != eScreenOrientation_PortraitPrimary &&
      aOrientation != eScreenOrientation_PortraitSecondary) {
    if (!mAutoOrientationEnabled) {
      EnableAutoOrientation();
    }
  } else if (mAutoOrientationEnabled) {
    DisableAutoOrientation();
  }

  mAllowedOrientations = aOrientation;

  nsCOMPtr<nsIScreen> screen = GetPrimaryScreen();
  NS_ENSURE_TRUE(screen, false);

  uint32_t currRotation;
  nsresult rv = screen->GetRotation(&currRotation);
  NS_ENSURE_SUCCESS(rv, false);

  ScreenOrientationInternal currOrientation = eScreenOrientation_None;
  rv = ConvertToDomOrientation(currRotation, &currOrientation);
  NS_ENSURE_SUCCESS(rv, false);

  // Don't rotate if the current orientation matches one of the
  // requested orientations.
  if (currOrientation & aOrientation) {
    return true;
  }

  // Return false on invalid orientation value.
  uint32_t rotation;
  rv = ConvertToScreenRotation(aOrientation, &rotation);
  NS_ENSURE_SUCCESS(rv, false);

  rv = screen->SetRotation(rotation);
  NS_ENSURE_SUCCESS(rv, false);

  // This conversion will disambiguate aOrientation.
  ScreenOrientationInternal orientation;
  rv = ConvertToDomOrientation(rotation, &orientation);
  NS_ENSURE_SUCCESS(rv, false);

  return true;
}

void
OrientationObserver::UnlockScreenOrientation()
{
  if (!mAutoOrientationEnabled) {
    EnableAutoOrientation();
  }

  mAllowedOrientations = sDefaultOrientations;
}
