/********************************************************************
 KWin - the KDE window manager
 This file is part of the KDE project.

Copyright (C) 1999, 2000 Matthias Ettrich <ettrich@kde.org>
Copyright (C) 2003 Lubos Lunak <l.lunak@kde.org>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/

// This file contains things relevant to handling incoming events.

#include "client.h"

#include <kstartupinfo.h>
#include <kglobal.h>
#include <X11/extensions/shape.h>

#include "notifications.h"
#include <QX11Info>
#include "rules.h"
#include "group.h"

namespace KWin
{

/**
 * Manages the clients. This means handling the very first maprequest:
 * reparenting, initial geometry, initial state, placement, etc.
 * Returns false if KWin is not going to manage this window.
 */
bool Client::manage( Window w, bool isMapped )
    {
    StackingUpdatesBlocker stacking_blocker( workspace() );

    grabXServer();

    XWindowAttributes attr;
    if( !XGetWindowAttributes( display(), w, &attr ))
        {
        ungrabXServer();
        return false;
        }

    // From this place on, manage() must not return false
    block_geometry_updates = 1;
    pending_geometry_update = PendingGeometryForced; // Force update when finishing with geometry changes

    embedClient( w, attr );

    vis = attr.visual;
    bit_depth = attr.depth;

    // SELI TODO: Order all these things in some sane manner

    bool init_minimize = false;
    XWMHints* hints = XGetWMHints( display(), w );
    if( hints && ( hints->flags & StateHint ) && hints->initial_state == IconicState )
        init_minimize = true;
    if( hints )
        XFree( hints );
    if( isMapped )
        init_minimize = false; // If it's already mapped, ignore hint

    unsigned long properties[2];
    properties[WinInfo::PROTOCOLS] =
        NET::WMDesktop |
        NET::WMState |
        NET::WMWindowType |
        NET::WMStrut |
        NET::WMName |
        NET::WMIconGeometry |
        NET::WMIcon |
        NET::WMPid |
        NET::WMIconName |
        0;
    properties[WinInfo::PROTOCOLS2] =
        NET::WM2UserTime |
        NET::WM2StartupId |
        NET::WM2ExtendedStrut |
        NET::WM2Opacity |
        NET::WM2FullscreenMonitors |
        0;

    info = new WinInfo( this, display(), client, rootWindow(), properties, 2 );

    cmap = attr.colormap;

    getResourceClass();
    getWindowRole();
    getWmClientLeader();
    getWmClientMachine();
    getSyncCounter();
    // First only read the caption text, so that setupWindowRules() can use it for matching,
    // and only then really set the caption using setCaption(), which checks for duplicates etc.
    // and also relies on rules already existing
    cap_normal = readName();
    setupWindowRules( false );
    ignore_focus_stealing = options->checkIgnoreFocusStealing( this ); // TODO: Change to rules
    setCaption( cap_normal, true );

    if( Extensions::shapeAvailable() )
        XShapeSelectInput( display(), window(), ShapeNotifyMask );
    detectShape( window() );
    detectNoBorder();
    fetchIconicName();
    getWMHints(); // Needs to be done before readTransient() because of reading the group
    modal = ( info->state() & NET::Modal ) != 0; // Needs to be valid before handling groups
    readTransient();
    getIcons();
    getWindowProtocols();
    getWmNormalHints(); // Get xSizeHint
    getMotifHints();

    // TODO: Try to obey all state information from info->state()

    original_skip_taskbar = skip_taskbar = ( info->state() & NET::SkipTaskbar ) != 0;
    skip_pager = ( info->state() & NET::SkipPager ) != 0;

    setupCompositing();

    KStartupInfoId asn_id;
    KStartupInfoData asn_data;
    bool asn_valid = workspace()->checkStartupNotification( window(), asn_id, asn_data );

    workspace()->updateClientLayer( this );

    SessionInfo* session = workspace()->takeSessionInfo( this );
    if( session )
        {
        init_minimize = session->minimized;
        noborder = session->noBorder;
        }

    setShortcut( rules()->checkShortcut( session ? session->shortcut : QString(), true ));

    init_minimize = rules()->checkMinimize( init_minimize, !isMapped );
    noborder = rules()->checkNoBorder( noborder, !isMapped );

    // Initial desktop placement
    if( session )
        {
        desk = session->desktop;
        if( session->onAllDesktops )
            desk = NET::OnAllDesktops;
        }
    else
        {
        // If this window is transient, ensure that it is opened on the
        // same window as its parent.  this is necessary when an application
        // starts up on a different desktop than is currently displayed
        if( isTransient() )
            {
            ClientList mainclients = mainClients();
            bool on_current = false;
            Client* maincl = NULL;
            // This is slightly duplicated from Placement::placeOnMainWindow()
            for( ClientList::ConstIterator it = mainclients.constBegin();
                it != mainclients.constEnd();
                ++it )
                {
                if( mainclients.count() > 1 && (*it)->isSpecialWindow() )
                    continue; // Don't consider toolbars etc when placing
                maincl = *it;
                if( (*it)->isOnCurrentDesktop() )
                    on_current = true;
                }
            if( on_current )
                desk = workspace()->currentDesktop();
            else if( maincl != NULL )
                desk = maincl->desktop();
            }
        if( info->desktop() )
            desk = info->desktop(); // Window had the initial desktop property, force it
        if( desktop() == 0 && asn_valid && asn_data.desktop() != 0 )
            desk = asn_data.desktop();
        }
    if( desk == 0 ) // Assume window wants to be visible on the current desktop
        desk = workspace()->currentDesktop();
    desk = rules()->checkDesktop( desk, !isMapped );
    if( desk != NET::OnAllDesktops ) // Do range check
        desk = qMax( 1, qMin( workspace()->numberOfDesktops(), desk ));
    info->setDesktop( desk );
    workspace()->updateOnAllDesktopsOfTransients( this ); // SELI TODO
    //onAllDesktopsChange(); // Decoration doesn't exist here yet

    QRect geom( attr.x, attr.y, attr.width, attr.height );
    bool placementDone = false;

    if ( session )
        geom = session->geometry;

    QRect area;
    bool partial_keep_in_area = isMapped || session;
    if( isMapped || session )
        area = workspace()->clientArea( FullArea, geom.center(), desktop() );
    else if( options->xineramaPlacementEnabled )
        {
        int screen = options->xineramaPlacementScreen;
        if( screen == -1 ) // Active screen
            screen = asn_data.xinerama() == -1 ? workspace()->activeScreen() : asn_data.xinerama();
        area = workspace()->clientArea( PlacementArea, workspace()->screenGeometry( screen ).center(), desktop());
        }
    else
        area = workspace()->clientArea( PlacementArea, cursorPos(), desktop() );

    if( int type = checkFullScreenHack( geom ))
        {
        fullscreen_mode = FullScreenHack;
        if( rules()->checkStrictGeometry( false ))
            {
            geom = type == 2 // 1 = It's xinerama-aware fullscreen hack, 2 = It's full area
                ? workspace()->clientArea( FullArea, geom.center(), desktop() )
                : workspace()->clientArea( ScreenArea, geom.center(), desktop() );
            }
        else
            geom = workspace()->clientArea( FullScreenArea, geom.center(), desktop() );
        placementDone = true;
        }

    if( isDesktop() )
        // KWin doesn't manage desktop windows
        placementDone = true;

    bool usePosition = false;
    if ( isMapped || session || placementDone )
        placementDone = true; // Use geometry
    else if( isTransient() && !isUtility() && !isDialog() && !isSplash() )
        usePosition = true;
    else if( isTransient() && !hasNETSupport() )
        usePosition = true;
    else if( isDialog() && hasNETSupport() )
        { // If the dialog is actually non-NETWM transient window, don't try to apply placement to it,
          // it breaks with too many things (xmms, display)
        if( mainClients().count() >= 1 )
            {
#if 1
            // #78082 - Ok, it seems there are after all some cases when an application has a good
            // reason to specify a position for its dialog. Too bad other WMs have never bothered
            // with placement for dialogs, so apps always specify positions for their dialogs,
            // including such silly positions like always centered on the screen or under mouse.
            // Using ignoring requested position in window-specific settings helps, and now
            // there's also _NET_WM_FULL_PLACEMENT.
            usePosition = true;
#else
            ; // Force using placement policy
#endif
            }
        else
            usePosition = true;
        }
    else if( isSplash() )
        ; // Force using placement policy
    else
        usePosition = true;
    if( !rules()->checkIgnoreGeometry( !usePosition ))
        {
        bool ignorePPosition = options->ignorePositionClasses.contains(
            QString::fromLatin1( resourceClass() ));

        if((( xSizeHint.flags & PPosition ) && !ignorePPosition ) ||
            ( xSizeHint.flags & USPosition ))
            {
            placementDone = true;
            // Disobey xinerama placement option for now (#70943)
            area = workspace()->clientArea( PlacementArea, geom.center(), desktop() );
            }
        }
    //if( true ) // Size is always obeyed for now, only with constraints applied
    //    if(( xSizeHint.flags & USSize ) || ( xSizeHint.flags & PSize ))
    //        {
    //        // Keep in mind that we now actually have a size :-)
    //        }

    if( xSizeHint.flags & PMaxSize )
        geom.setSize( geom.size().boundedTo(
            rules()->checkMaxSize( QSize( xSizeHint.max_width, xSizeHint.max_height ))));
    if( xSizeHint.flags & PMinSize )
        geom.setSize( geom.size().expandedTo(
            rules()->checkMinSize( QSize( xSizeHint.min_width, xSizeHint.min_height ))));

    if( isMovable() && ( geom.x() > area.right() || geom.y() > area.bottom() ))
        placementDone = false; // Weird, do not trust.

    if( placementDone )
        move( geom.x(), geom.y() ); // Before gravitating

    updateDecoration( false ); // Also gravitates
    // TODO: Is CentralGravity right here, when resizing is done after gravitating?
    plainResize( rules()->checkSize( sizeForClientSize( geom.size() ), !isMapped ));

    QPoint forced_pos = rules()->checkPosition( invalidPoint, !isMapped );
    if( forced_pos != invalidPoint )
        {
        move( forced_pos );
        placementDone = true;
        // Don't keep inside workarea if the window has specially configured position
        partial_keep_in_area = true;
        area = workspace()->clientArea( FullArea, geom.center(), desktop() );
        }
    if( !placementDone )
        { // Placement needs to be after setting size
        workspace()->place( this, area );
        placementDone = true;
        }

    if(( !isSpecialWindow() || isToolbar() ) && isMovable() )
        keepInArea( area, partial_keep_in_area );

    updateShape();

    // CT: Extra check for stupid jdk 1.3.1. But should make sense in general
    // if client has initial state set to Iconic and is transient with a parent
    // window that is not Iconic, set init_state to Normal
    if( init_minimize && isTransient() )
        {
        ClientList mainclients = mainClients();
        for( ClientList::ConstIterator it = mainclients.constBegin();
            it != mainclients.constEnd();
            ++it )
            if( (*it)->isShown( true ))
                init_minimize = false; // SELI TODO: Even e.g. for NET::Utility?
        }
    // If a dialog is shown for minimized window, minimize it too
    if( !init_minimize && isTransient() && mainClients().count() > 0 )
        {
        bool visible_parent = false;
        // Use allMainClients(), to include also main clients of group transients
        // that have been optimized out in Client::checkGroupTransients()
        ClientList mainclients = allMainClients();
        for( ClientList::ConstIterator it = mainclients.constBegin();
            it != mainclients.constEnd();
            ++it )
            if( (*it)->isShown( true ))
                visible_parent = true;
        if( !visible_parent )
            {
            init_minimize = true;
            demandAttention();
            }
        }

    if( init_minimize )
        minimize( true ); // No animation

    // SELI TODO: This seems to be mainly for kstart and ksystraycmd
    // probably should be replaced by something better
    bool doNotShow = false;
    if ( workspace()->isNotManaged( caption() ))
        doNotShow = true;

    // Other settings from the previous session
    if( session )
        {
        // Session restored windows are not considered to be new windows WRT rules,
        // I.e. obey only forcing rules
        setKeepAbove( session->keepAbove );
        setKeepBelow( session->keepBelow );
        setSkipTaskbar( session->skipTaskbar, true );
        setSkipPager( session->skipPager );
        setShade( session->shaded ? ShadeNormal : ShadeNone );
        if( session->maximized != MaximizeRestore )
            {
            maximize( MaximizeMode( session->maximized ));
            geom_restore = session->restore;
            }
        if( session->fullscreen == FullScreenHack )
            ; // Nothing, this should be already set again above
        else if( session->fullscreen != FullScreenNone )
            {
            setFullScreen( true, false );
            geom_fs_restore = session->fsrestore;
            }
        }
    else
        {
        geom_restore = geometry(); // Remember restore geometry
        if( isMaximizable() && ( width() >= area.width() || height() >= area.height() ))
            { // Window is too large for the screen, maximize in the
              // directions necessary
            if( width() >= area.width() && height() >= area.height() )
                {
                maximize( Client::MaximizeFull );
                geom_restore = QRect(); // Use placement when unmaximizing
                }
            else if( width() >= area.width() )
                {
                maximize( Client::MaximizeHorizontal );
                geom_restore = QRect(); // Use placement when unmaximizing
                geom_restore.setY( y() ); // But only for horizontal direction
                geom_restore.setHeight( height() );
                }
            else if( height() >= area.height() )
                {
                maximize( Client::MaximizeVertical );
                geom_restore = QRect(); // Use placement when unmaximizing
                geom_restore.setX( x() ); // But only for vertical direction
                geom_restore.setWidth( width() );
                }
            }

        // Window may want to be maximized
        // done after checking that the window isn't larger than the workarea, so that
        // the restore geometry from the checks above takes precedence, and window
        // isn't restored larger than the workarea
        MaximizeMode maxmode = static_cast<MaximizeMode>(
            (( info->state() & NET::MaxVert ) ? MaximizeVertical : 0 ) |
            (( info->state() & NET::MaxHoriz ) ? MaximizeHorizontal : 0 ));
        MaximizeMode forced_maxmode = rules()->checkMaximize( maxmode, !isMapped );

        // Either hints were set to maximize, or is forced to maximize,
        // or is forced to non-maximize and hints were set to maximize
        if( forced_maxmode != MaximizeRestore || maxmode != MaximizeRestore )
            maximize( forced_maxmode );

        // Read other initial states
        setShade( rules()->checkShade( info->state() & NET::Shaded ? ShadeNormal : ShadeNone, !isMapped ));
        setKeepAbove( rules()->checkKeepAbove( info->state() & NET::KeepAbove, !isMapped ));
        setKeepBelow( rules()->checkKeepBelow( info->state() & NET::KeepBelow, !isMapped ));
        setSkipTaskbar( rules()->checkSkipTaskbar( info->state() & NET::SkipTaskbar, !isMapped ), true );
        setSkipPager( rules()->checkSkipPager( info->state() & NET::SkipPager, !isMapped ));
        if( info->state() & NET::DemandsAttention )
            demandAttention();
        if( info->state() & NET::Modal )
            setModal( true );
        if( fullscreen_mode != FullScreenHack && isFullScreenable() )
            setFullScreen( rules()->checkFullScreen( info->state() & NET::FullScreen, !isMapped ), false );
        }

    updateAllowedActions( true );

    // Set initial user time directly
    user_time = readUserTimeMapTimestamp( asn_valid ? &asn_id : NULL, asn_valid ? &asn_data : NULL, session );
    group()->updateUserTime( user_time ); // And do what Client::updateUserTime() does

    if( isTopMenu()) // They're shown in Workspace::addClient() if their mainwindow
        hideClient( true ); // Is the active one

    // This should avoid flicker, because real restacking is done
    // only after manage() finishes because of blocking, but the window is shown sooner
    XLowerWindow( display(), frameId() );
    if( session && session->stackingOrder != -1 )
        {
        sm_stacking_order = session->stackingOrder;
        workspace()->restoreSessionStackingOrder( this );
        }

    if( compositing() )
        // Sending ConfigureNotify is done when setting mapping state below,
        // Getting the first sync response means window is ready for compositing
        sendSyncRequest();

    if( isShown( true ) && !doNotShow )
        {
        if( isDialog() )
            Notify::raise( Notify::TransNew );
        if( isNormalWindow() )
            Notify::raise( Notify::New );

        bool allow;
        if( session )
            allow = session->active &&
                ( !workspace()->wasUserInteraction() || workspace()->activeClient() == NULL ||
                workspace()->activeClient()->isDesktop() );
        else
            allow = workspace()->allowClientActivation( this, userTime(), false );

        // If session saving, force showing new windows (i.e. "save file?" dialogs etc.)
        // also force if activation is allowed
        if( !isOnCurrentDesktop() && !isMapped && !session && ( allow || workspace()->sessionSaving() ))
            workspace()->setCurrentDesktop( desktop() );

        bool belongs_to_desktop = false;
        for( ClientList::ConstIterator it = group()->members().constBegin();
            it != group()->members().constEnd();
            ++it )
            if( (*it)->isDesktop() )
                {
                belongs_to_desktop = true;
                break;
                }
        if( !belongs_to_desktop && workspace()->showingDesktop() )
            workspace()->resetShowingDesktop( options->showDesktopIsMinimizeAll );

        if( isOnCurrentDesktop() && !isMapped && !allow && ( !session || session->stackingOrder < 0 ))
            workspace()->restackClientUnderActive( this );

        updateVisibility();

        if( !isMapped )
            {
            if( allow && isOnCurrentDesktop() )
                {
                if( !isSpecialWindow() )
                    if ( options->focusPolicyIsReasonable() && wantsTabFocus() )
                        workspace()->requestFocus( this );
                }
            else if( !session && !isSpecialWindow())
                demandAttention();
            }
        }
    else if( !doNotShow ) // if( !isShown( true ) && !doNotShow )
        updateVisibility();
    else // doNotShow
        hideClient( true ); // SELI HACK !!!
    assert( mapping_state != Withdrawn );
    blockGeometryUpdates( false );

    if( user_time == CurrentTime || user_time == -1U )
        { // No known user time, set something old
        user_time = xTime() - 1000000;
        if( user_time == CurrentTime || user_time == -1U ) // Let's be paranoid
            user_time = xTime() - 1000000 + 10;
        }

    updateWorkareaDiffs();

    //sendSyntheticConfigureNotify(); // Done when setting mapping state

    delete session;

    ungrabXServer();

    client_rules.discardTemporary();
    applyWindowRules(); // Just in case
    workspace()->discardUsedWindowRules( this, false ); // Remove ApplyNow rules
    updateWindowRules(); // Was blocked while !isManaged()

    // TODO: there's a small problem here - isManaged() depends on the mapping state,
    // but this client is not yet in Workspace's client list at this point, will
    // be only done in addClient()
    return true;
    }

// Called only from manage()
void Client::embedClient( Window w, const XWindowAttributes& attr )
    {
    assert( client == None );
    assert( frameId() == None );
    assert( wrapper == None );
    client = w;

    // We don't want the window to be destroyed when we are destroyed
    XAddToSaveSet( display(), client );
    XSelectInput( display(), client, NoEventMask );
    XUnmapWindow( display(), client );
    XWindowChanges wc; // Set the border width to 0
    wc.border_width = 0; // TODO: Possibly save this, and also use it for initial configuring of the window
    XConfigureWindow( display(), client, CWBorderWidth, &wc );

    XSetWindowAttributes swa;
    swa.colormap = attr.colormap;
    swa.background_pixmap = None;
    swa.border_pixel = 0;

    Window frame = XCreateWindow( display(), rootWindow(), 0, 0, 1, 1, 0,
        attr.depth, InputOutput, attr.visual, CWColormap | CWBackPixmap | CWBorderPixel, &swa );
    setWindowHandles( client, frame );
    wrapper = XCreateWindow( display(), frame, 0, 0, 1, 1, 0,
        attr.depth, InputOutput, attr.visual, CWColormap | CWBackPixmap | CWBorderPixel, &swa );

    XDefineCursor( display(), frame, QCursor( Qt::ArrowCursor ).handle() );
    // Some apps are stupid and don't define their own cursor - set the arrow one for them
    XDefineCursor( display(), wrapper, QCursor( Qt::ArrowCursor ).handle() );
    XReparentWindow( display(), client, wrapper, 0, 0 );
    XSelectInput( display(), frame,
        KeyPressMask | KeyReleaseMask |
        ButtonPressMask | ButtonReleaseMask |
        KeymapStateMask |
        ButtonMotionMask |
        PointerMotionMask |
        EnterWindowMask | LeaveWindowMask |
        FocusChangeMask |
        ExposureMask |
        PropertyChangeMask |
        StructureNotifyMask | SubstructureRedirectMask );
    XSelectInput( display(), wrapper, ClientWinMask | SubstructureNotifyMask );
    XSelectInput( display(), client,
        FocusChangeMask |
        PropertyChangeMask |
        ColormapChangeMask |
        EnterWindowMask | LeaveWindowMask |
        KeyPressMask | KeyReleaseMask
        );

    updateMouseGrab();
    }

} // namespace
