/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qiosglobal.h"
#import "qiosviewcontroller.h"

#include <QtCore/qscopedvaluerollback.h>
#include <QtCore/private/qcore_mac_p.h>

#include <QtGui/QGuiApplication>
#include <QtGui/QWindow>
#include <QtGui/QScreen>

#include <QtGui/private/qwindow_p.h>

#include "qiosintegration.h"
#include "qiosscreen.h"
#include "qiosglobal.h"
#include "qioswindow.h"
#include "quiview.h"

// -------------------------------------------------------------------------

@interface QIOSViewController ()
@property (nonatomic, assign) QPointer<QT_PREPEND_NAMESPACE(QIOSScreen)> platformScreen;
@property (nonatomic, assign) BOOL changingOrientation;
@end

// -------------------------------------------------------------------------

@interface QIOSDesktopManagerView : UIView
@end

@implementation QIOSDesktopManagerView

- (instancetype)init
{
    if (!(self = [super init]))
        return nil;

    if (qEnvironmentVariableIntValue("QT_IOS_DEBUG_WINDOW_MANAGEMENT")) {
        static UIImage *gridPattern = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            CGFloat dimension = 100.f;

            UIGraphicsBeginImageContextWithOptions(CGSizeMake(dimension, dimension), YES, 0.0f);
            CGContextRef context = UIGraphicsGetCurrentContext();

            CGContextTranslateCTM(context, -0.5, -0.5);

            #define gridColorWithBrightness(br) \
                [UIColor colorWithHue:0.6 saturation:0.0 brightness:br alpha:1.0].CGColor

            CGContextSetFillColorWithColor(context, gridColorWithBrightness(0.05));
            CGContextFillRect(context, CGRectMake(0, 0, dimension, dimension));

            CGFloat gridLines[][2] = { { 10, 0.1 }, { 20, 0.2 }, { 100, 0.3 } };
            for (size_t l = 0; l < sizeof(gridLines) / sizeof(gridLines[0]); ++l) {
                CGFloat step = gridLines[l][0];
                for (int c = step; c <= dimension; c += step) {
                    CGContextMoveToPoint(context, c, 0);
                    CGContextAddLineToPoint(context, c, dimension);
                    CGContextMoveToPoint(context, 0, c);
                    CGContextAddLineToPoint(context, dimension, c);
                }

                CGFloat brightness = gridLines[l][1];
                CGContextSetStrokeColorWithColor(context, gridColorWithBrightness(brightness));
                CGContextStrokePath(context);
            }

            gridPattern = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();

            [gridPattern retain];
        });

        self.backgroundColor = [UIColor colorWithPatternImage:gridPattern];
    }

    return self;
}

- (void)didAddSubview:(UIView *)subview
{
    Q_UNUSED(subview);

    QT_PREPEND_NAMESPACE(QIOSScreen) *screen = self.qtViewController.platformScreen;

    // The 'window' property of our view is not valid until the window
    // has been shown, so we have to access it through the QIOSScreen.
    UIWindow *uiWindow = screen->uiWindow();

    if (uiWindow.hidden) {
        // Associate UIWindow to screen and show it the first time a QWindow
        // is mapped to the screen. For external screens this means disabling
        // mirroring mode and presenting alternate content on the screen.
        uiWindow.screen = screen->uiScreen();
        uiWindow.hidden = NO;
    }
}

- (void)willRemoveSubview:(UIView *)subview
{
    Q_UNUSED(subview);

    Q_ASSERT(self.window);
    UIWindow *uiWindow = self.window;

    if (uiWindow.screen != [UIScreen mainScreen] && self.subviews.count == 1) {
        // Removing the last view of an external screen, go back to mirror mode
        uiWindow.screen = [UIScreen mainScreen];
        uiWindow.hidden = YES;
    }
}

- (void)layoutSubviews
{
    if (QGuiApplication::applicationState() == Qt::ApplicationSuspended) {
        // Despite the OpenGL ES Programming Guide telling us to avoid all
        // use of OpenGL while in the background, iOS will perform its view
        // snapshotting for the app switcher after the application has been
        // backgrounded; once for each orientation. Presumably the expectation
        // is that no rendering needs to be done to provide an alternate
        // orientation snapshot, just relayouting of views. But in our case,
        // or any non-stretchable content case such as a OpenGL based game,
        // this is not true. Instead of continuing layout, which will send
        // potentially expensive geometry changes (with isExposed false,
        // since we're in the background), we short-circuit the snapshotting
        // here. iOS will still use the latest rendered frame to create the
        // application switcher thumbnail, but it will be based on the last
        // active orientation of the application.
        QIOSScreen *screen = self.qtViewController.platformScreen;
        qCDebug(lcQpaWindow) << "ignoring layout of subviews while suspended,"
            << "likely system snapshot of" << screen->screen()->primaryOrientation();
        return;
    }

    for (int i = int(self.subviews.count) - 1; i >= 0; --i) {
        UIView *view = static_cast<UIView *>([self.subviews objectAtIndex:i]);
        if (![view isKindOfClass:[QUIView class]])
            continue;

        [self layoutView: static_cast<QUIView *>(view)];
    }
}

- (void)layoutView:(QUIView *)view
{
    QWindow *window = view.qwindow;

    // Return early if the QIOSWindow is still constructing, as we'll
    // take care of setting the correct window state in the constructor.
    if (!window->handle())
        return;

    // Re-apply window states to update geometry
    if (window->windowStates() & (Qt::WindowFullScreen | Qt::WindowMaximized))
        window->handle()->setWindowState(window->windowStates());
}

// Even if the root view controller has both wantsFullScreenLayout and
// extendedLayoutIncludesOpaqueBars enabled, iOS will still push the root
// view down 20 pixels (and shrink the view accordingly) when the in-call
// statusbar is active (instead of updating the topLayoutGuide). Since
// we treat the root view controller as our screen, we want to reflect
// the in-call statusbar as a change in available geometry, not in screen
// geometry. To simplify the screen geometry mapping code we reset the
// view modifications that iOS does and take the statusbar height
// explicitly into account in QIOSScreen::updateProperties().

- (void)setFrame:(CGRect)newFrame
{
    Q_UNUSED(newFrame);
    Q_ASSERT(!self.window || self.window.rootViewController.view == self);

    // When presenting view controllers our view may be temporarily reparented into a UITransitionView
    // instead of the UIWindow, and the UITransitionView may have a transform set, so we need to do a
    // mapping even if we still expect to always be the root view-controller.
    CGRect transformedWindowBounds = [self.superview convertRect:self.window.bounds fromView:self.window];
    [super setFrame:transformedWindowBounds];
}

- (void)setBounds:(CGRect)newBounds
{
    Q_UNUSED(newBounds);
    CGRect transformedWindowBounds = [self convertRect:self.window.bounds fromView:self.window];
    [super setBounds:CGRectMake(0, 0, CGRectGetWidth(transformedWindowBounds), CGRectGetHeight(transformedWindowBounds))];
}

- (void)setCenter:(CGPoint)newCenter
{
    Q_UNUSED(newCenter);
    [super setCenter:self.window.center];
}

- (void)didMoveToWindow
{
    // The initial frame computed during startup may happen before the view has
    // a window, meaning our calculations above will be wrong. We ensure that the
    // frame is set correctly once we have a window to base our calulations on.
    [self setFrame:self.window.bounds];
}

@end

// -------------------------------------------------------------------------

@implementation QIOSViewController {
    BOOL m_updatingProperties;
    QMetaObject::Connection m_focusWindowChangeConnection;
}

#ifndef Q_OS_TVOS
@synthesize prefersStatusBarHidden;
@synthesize preferredStatusBarUpdateAnimation;
@synthesize preferredStatusBarStyle;
#endif

- (instancetype)initWithQIOSScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen
{
    if (self = [self init]) {
        self.platformScreen = screen;

        self.changingOrientation = NO;
#ifndef Q_OS_TVOS
        self.lockedOrientation = UIInterfaceOrientationUnknown;

        // Status bar may be initially hidden at startup through Info.plist
        self.prefersStatusBarHidden = infoPlistValue(@"UIStatusBarHidden", false);
        self.preferredStatusBarUpdateAnimation = UIStatusBarAnimationNone;
        self.preferredStatusBarStyle = UIStatusBarStyle(infoPlistValue(@"UIStatusBarStyle", UIStatusBarStyleDefault));
#endif

        m_focusWindowChangeConnection = QObject::connect(qApp, &QGuiApplication::focusWindowChanged, [self]() {
            [self updateProperties];
        });

        QIOSApplicationState *applicationState = &QIOSIntegration::instance()->applicationState;
        QObject::connect(applicationState, &QIOSApplicationState::applicationStateDidChange,
            [self](Qt::ApplicationState oldState, Qt::ApplicationState newState) {
                if (oldState == Qt::ApplicationSuspended && newState != Qt::ApplicationSuspended) {
                    // We may have ignored an earlier layout because the application was suspended,
                    // and we didn't want to render anything at that moment in fear of being killed
                    // due to rendering in the background, so we trigger an explicit layout when
                    // coming out of the suspended state.
                    qCDebug(lcQpaWindow) << "triggering root VC layout when coming out of suspended state";
                    [self.view setNeedsLayout];
                }
            }
        );
    }

    return self;
}

- (void)dealloc
{
    QObject::disconnect(m_focusWindowChangeConnection);
    [super dealloc];
}

- (void)loadView
{
    self.view = [[[QIOSDesktopManagerView alloc] init] autorelease];
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    Q_ASSERT(!qt_apple_isApplicationExtension());

#ifndef Q_OS_TVOS
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(willChangeStatusBarFrame:)
            name:UIApplicationWillChangeStatusBarFrameNotification
            object:qt_apple_sharedApplication()];

    [center addObserver:self selector:@selector(didChangeStatusBarOrientation:)
            name:UIApplicationDidChangeStatusBarOrientationNotification
            object:qt_apple_sharedApplication()];
#endif
}

- (void)viewDidUnload
{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:nil];
    [super viewDidUnload];
}

// -------------------------------------------------------------------------

- (BOOL)shouldAutorotate
{
#ifndef Q_OS_TVOS
    return self.platformScreen && self.platformScreen->uiScreen() == [UIScreen mainScreen] && !self.lockedOrientation;
#else
    return NO;
#endif
}

- (NSUInteger)supportedInterfaceOrientations
{
    // As documented by Apple in the iOS 6.0 release notes, setStatusBarOrientation:animated:
    // only works if the supportedInterfaceOrientations of the view controller is 0, making
    // us responsible for ensuring that the status bar orientation is consistent. We enter
    // this mode when auto-rotation is disabled due to an explicit content orientation being
    // set on the focus window. Note that this is counter to what the documentation for
    // supportedInterfaceOrientations says, which states that the method should not return 0.
    return [self shouldAutorotate] ? UIInterfaceOrientationMaskAll : 0;
}

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration
{
    self.changingOrientation = YES;

    [super willRotateToInterfaceOrientation:orientation duration:duration];
}

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)orientation
{
    self.changingOrientation = NO;

    [super didRotateFromInterfaceOrientation:orientation];
}

- (void)willChangeStatusBarFrame:(NSNotification*)notification
{
    Q_UNUSED(notification);

    if (self.view.window.screen != [UIScreen mainScreen])
        return;

    // Orientation changes will already result in laying out subviews, so we don't
    // need to do anything extra for frame changes during an orientation change.
    // Technically we can receive another actual statusbar frame update during the
    // orientation change that we should react to, but to simplify the logic we
    // use a simple bool variable instead of a ignoreNextFrameChange approach.
    if (self.changingOrientation)
        return;

    // UIKit doesn't have a delegate callback for statusbar changes that's run inside the
    // animation block, like UIViewController's willAnimateRotationToInterfaceOrientation,
    // nor does it expose a constant for the duration and easing of the animation. However,
    // though poking at the various UIStatusBar methods, we can observe that the animation
    // uses the default easing curve, and runs with a duration of 0.35 seconds.
    static qreal kUIStatusBarAnimationDuration = 0.35;

    [UIView animateWithDuration:kUIStatusBarAnimationDuration animations:^{
        [self.view setNeedsLayout];
        [self.view layoutIfNeeded];
    }];
}

- (void)didChangeStatusBarOrientation:(NSNotification *)notification
{
    Q_UNUSED(notification);

    if (self.view.window.screen != [UIScreen mainScreen])
        return;

    // If the statusbar changes orientation due to auto-rotation we don't care,
    // there will be re-layout anyways. Only if the statusbar changes due to
    // reportContentOrientation, we need to update the window layout.
    if (self.changingOrientation)
        return;

    [self.view setNeedsLayout];
}

- (void)viewWillLayoutSubviews
{
    if (!QCoreApplication::instance())
        return;

    if (self.platformScreen)
        self.platformScreen->updateProperties();
}

// -------------------------------------------------------------------------

- (void)updateProperties
{
    if (!isQtApplication())
        return;

    if (!self.platformScreen || !self.platformScreen->screen())
        return;

    // For now we only care about the main screen, as both the statusbar
    // visibility and orientation is only appropriate for the main screen.
    if (self.platformScreen->uiScreen() != [UIScreen mainScreen])
        return;

    // Prevent recursion caused by updating the status bar appearance (position
    // or visibility), which in turn may cause a layout of our subviews, and
    // a reset of window-states, which themselves affect the view controller
    // properties such as the statusbar visibilty.
    if (m_updatingProperties)
        return;

    QScopedValueRollback<BOOL> updateRollback(m_updatingProperties, YES);

    QWindow *focusWindow = QGuiApplication::focusWindow();

    // If we don't have a focus window we leave the statusbar
    // as is, so that the user can activate a new window with
    // the same window state without the status bar jumping
    // back and forth.
    if (!focusWindow)
        return;

    // We only care about changes to focusWindow that involves our screen
    if (!focusWindow->screen() || focusWindow->screen()->handle() != self.platformScreen)
        return;

    // All decisions are based on the top level window
    focusWindow = qt_window_private(focusWindow)->topLevelWindow();

#ifndef Q_OS_TVOS

    // -------------- Status bar style and visbility ---------------

    UIStatusBarStyle oldStatusBarStyle = self.preferredStatusBarStyle;
    if (focusWindow->flags() & Qt::MaximizeUsingFullscreenGeometryHint)
        self.preferredStatusBarStyle = UIStatusBarStyleDefault;
    else
        self.preferredStatusBarStyle = UIStatusBarStyleLightContent;

    if (self.preferredStatusBarStyle != oldStatusBarStyle)
        [self setNeedsStatusBarAppearanceUpdate];

    bool currentStatusBarVisibility = self.prefersStatusBarHidden;
    self.prefersStatusBarHidden = focusWindow->windowState() == Qt::WindowFullScreen;

    if (self.prefersStatusBarHidden != currentStatusBarVisibility) {
        [self setNeedsStatusBarAppearanceUpdate];
        [self.view setNeedsLayout];
    }


    // -------------- Content orientation ---------------

    UIApplication *uiApplication = qt_apple_sharedApplication();

    static BOOL kAnimateContentOrientationChanges = YES;

    Qt::ScreenOrientation contentOrientation = focusWindow->contentOrientation();
    if (contentOrientation != Qt::PrimaryOrientation) {
        // An explicit content orientation has been reported for the focus window,
        // so we keep the status bar in sync with content orientation. This will ensure
        // that the task bar (and associated gestures) are also rotated accordingly.

        if (!self.lockedOrientation) {
            // We are moving from Qt::PrimaryOrientation to an explicit orientation,
            // so we need to store the current statusbar orientation, as we need it
            // later when mapping screen coordinates for QScreen and for returning
            // to Qt::PrimaryOrientation.
            self.lockedOrientation = uiApplication.statusBarOrientation;
        }

        [uiApplication setStatusBarOrientation:
            UIInterfaceOrientation(fromQtScreenOrientation(contentOrientation))
            animated:kAnimateContentOrientationChanges];

    } else {
        // The content orientation is set to Qt::PrimaryOrientation, meaning
        // that auto-rotation should be enabled. But we may be coming out of
        // a state of locked orientation, which needs some cleanup before we
        // can enable auto-rotation again.
        if (self.lockedOrientation) {
            // First we need to restore the statusbar to what it was at the
            // time of locking the orientation, otherwise iOS will be very
            // confused when it starts doing auto-rotation again.
            [uiApplication setStatusBarOrientation:self.lockedOrientation
                animated:kAnimateContentOrientationChanges];

            // Then we can re-enable auto-rotation
            self.lockedOrientation = UIInterfaceOrientationUnknown;

            // And finally let iOS rotate the root view to match the device orientation
            [UIViewController attemptRotationToDeviceOrientation];
        }
    }
#endif
}

@end

