/*
 * Copyright (C) 2009 Apple Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#import "config.h"

#if ENABLE(GEOLOCATION)

#import "GeolocationServiceMac.h"

#import "Geoposition.h"
#import "PositionError.h"
#import "PositionOptions.h"
#import "SoftLinking.h"
#import <CoreLocation/CoreLocation.h>
#import <objc/objc-runtime.h>
#import <wtf/RefPtr.h>
#import <wtf/UnusedParam.h>

SOFT_LINK_FRAMEWORK(CoreLocation)

SOFT_LINK_CLASS(CoreLocation, CLLocationManager)
SOFT_LINK_CLASS(CoreLocation, CLLocation)

SOFT_LINK_CONSTANT(CoreLocation, kCLLocationAccuracyBest, double)
SOFT_LINK_CONSTANT(CoreLocation, kCLLocationAccuracyHundredMeters, double)

#define kCLLocationAccuracyBest getkCLLocationAccuracyBest()
#define kCLLocationAccuracyHundredMeters getkCLLocationAccuracyHundredMeters()

using namespace WebCore;

@interface WebCoreCoreLocationObserver : NSObject<CLLocationManagerDelegate>
{
    GeolocationServiceMac* m_callback;
}

- (id)initWithCallback:(GeolocationServiceMac*)callback;

- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation;
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error;

@end

namespace WebCore {

GeolocationService* GeolocationServiceMac::create(GeolocationServiceClient* client)
{
    return new GeolocationServiceMac(client);
}

GeolocationService::FactoryFunction* GeolocationService::s_factoryFunction = &GeolocationServiceMac::create;

GeolocationServiceMac::GeolocationServiceMac(GeolocationServiceClient* client)
    : GeolocationService(client)
    , m_objcObserver(AdoptNS, [[WebCoreCoreLocationObserver alloc] initWithCallback:this])
{
}

GeolocationServiceMac::~GeolocationServiceMac()
{
    [m_locationManager.get() stopUpdatingLocation];
    m_locationManager.get().delegate = nil;
}

bool GeolocationServiceMac::startUpdating(PositionOptions* options)
{
    #define CLLocationManager getCLLocationManagerClass()
    if (!m_locationManager.get()) {
        m_locationManager.adoptNS([[CLLocationManager alloc] init]);
        m_locationManager.get().delegate = m_objcObserver.get();
    }

    if (!m_locationManager.get().locationServicesEnabled)
        return false;

    if (options) {
        // CLLocationAccuracy values suggested by Ron Huang.
        CLLocationAccuracy accuracy = options->enableHighAccuracy() ? kCLLocationAccuracyBest : kCLLocationAccuracyHundredMeters;
        m_locationManager.get().desiredAccuracy = accuracy;
    }
    
    // This can safely be called multiple times.
    [m_locationManager.get() startUpdatingLocation];
    
    return true;
    #undef CLLocationManager
}

void GeolocationServiceMac::stopUpdating()
{
    [m_locationManager.get() stopUpdatingLocation];
}

void GeolocationServiceMac::suspend()
{
    [m_locationManager.get() stopUpdatingLocation];
}

void GeolocationServiceMac::resume()
{
    [m_locationManager.get() startUpdatingLocation];
}

void GeolocationServiceMac::positionChanged(PassRefPtr<Geoposition> position)
{
    m_lastPosition = position;
    GeolocationService::positionChanged();
}
    
void GeolocationServiceMac::errorOccurred(PassRefPtr<PositionError> error)
{
    m_lastError = error;
    GeolocationService::errorOccurred();
}

} // namespace WebCore

@implementation WebCoreCoreLocationObserver

- (id)initWithCallback:(GeolocationServiceMac *)callback
{
    self = [super init];
    if (self)
        m_callback = callback;
    return self;
}

- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
    ASSERT(m_callback);
    ASSERT(newLocation);
    UNUSED_PARAM(manager);
    UNUSED_PARAM(oldLocation);

    // Normalize
    bool canProvideAltitude = true;
    bool canProvideAltitudeAccuracy = true;
    double altitude = newLocation.altitude;
    double altitudeAccuracy = newLocation.verticalAccuracy;
    if (altitudeAccuracy < 0.0) {
        canProvideAltitude = false;
        canProvideAltitudeAccuracy = false;
    }
    
    bool canProvideSpeed = true;
    double speed = newLocation.speed;
    if (speed < 0.0)
        canProvideSpeed = false;

    bool canProvideHeading = true;
    double heading = newLocation.course;
    if (heading < 0.0)
        canProvideHeading = false;
    
    WTF::RefPtr<WebCore::Coordinates> newCoordinates = WebCore::Coordinates::create(
                            newLocation.coordinate.latitude,
                            newLocation.coordinate.longitude,
                            canProvideAltitude,
                            altitude,
                            newLocation.horizontalAccuracy,
                            canProvideAltitudeAccuracy,
                            altitudeAccuracy,
                            canProvideHeading,
                            heading,
                            canProvideSpeed,
                            speed);
    WTF::RefPtr<WebCore::Geoposition> newPosition = WebCore::Geoposition::create(
                             newCoordinates.release(),
                             [newLocation.timestamp timeIntervalSince1970] * 1000.0); // seconds -> milliseconds
    
    m_callback->positionChanged(newPosition.release());
}

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
    ASSERT(m_callback);
    ASSERT(error);
 
    UNUSED_PARAM(manager);

    PositionError::ErrorCode code;
    switch ([error code]) {
        case kCLErrorDenied:
            code = PositionError::PERMISSION_DENIED;
            break;
        case kCLErrorLocationUnknown:
            code = PositionError::POSITION_UNAVAILABLE;
            break;
        default:
            code = PositionError::POSITION_UNAVAILABLE;
            break;
    }

    m_callback->errorOccurred(PositionError::create(code, [error localizedDescription]));
}

@end

#endif // ENABLE(GEOLOCATION)
