/*
 * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.awt;

import java.awt.DisplayMode;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.geom.Rectangle2D;
import java.awt.peer.WindowPeer;
import java.util.Arrays;
import java.util.Objects;

import sun.java2d.SunGraphicsEnvironment;
import sun.java2d.MacOSFlags;
import sun.java2d.metal.MTLGraphicsConfig;
import sun.java2d.opengl.CGLGraphicsConfig;

import static java.awt.peer.ComponentPeer.SET_BOUNDS;

public final class CGraphicsDevice extends GraphicsDevice
        implements DisplayChangedListener {

    /**
     * CoreGraphics display ID. This identifier can become non-valid at any time
     * therefore methods, which is using this id should be ready to it.
     */
    private volatile int displayID;
    private volatile double xResolution;
    private volatile double yResolution;
    private volatile Rectangle bounds;
    private volatile int scale;

    private GraphicsConfiguration config;
    private static boolean metalPipelineEnabled = false;
    private static boolean oglPipelineEnabled = false;


    // Save/restore DisplayMode for the Full Screen mode
    private DisplayMode originalMode;
    private DisplayMode initialMode;

    public CGraphicsDevice(final int displayID) {
        this.displayID = displayID;
        this.initialMode = getDisplayMode();

        if (MacOSFlags.isMetalEnabled()) {
            // Try to create MTLGraphicsConfig, if it fails,
            // try to create CGLGraphicsConfig as a fallback
            this.config = MTLGraphicsConfig.getConfig(this, displayID);

            if (this.config != null) {
                metalPipelineEnabled = true;
            } else {
                // Try falling back to OpenGL pipeline
                if (MacOSFlags.isMetalVerbose()) {
                    System.out.println("Metal rendering pipeline" +
                        " initialization failed,using OpenGL" +
                        " rendering pipeline");
                }

                this.config = CGLGraphicsConfig.getConfig(this);

                if (this.config != null) {
                    oglPipelineEnabled = true;
                }
            }
        } else {
            // Try to create CGLGraphicsConfig, if it fails,
            // try to create MTLGraphicsConfig as a fallback
            this.config = CGLGraphicsConfig.getConfig(this);

            if (this.config != null) {
                oglPipelineEnabled = true;
            } else {
                // Try falling back to Metal pipeline
                if (MacOSFlags.isOGLVerbose()) {
                    System.out.println("OpenGL rendering pipeline" +
                        " initialization failed,using Metal" +
                        " rendering pipeline");
                }

                this.config = MTLGraphicsConfig.getConfig(this, displayID);

                if (this.config != null) {
                    metalPipelineEnabled = true;
                }
            }
        }

        if (!metalPipelineEnabled && !oglPipelineEnabled) {
            // This indicates fallback to other rendering pipeline also failed.
            // Should never reach here
            throw new InternalError("Error - unable to initialize any" +
                " rendering pipeline.");
        }

        if (metalPipelineEnabled && MacOSFlags.isMetalVerbose()) {
            System.out.println("Metal pipeline enabled on screen " + displayID);
        } else if (oglPipelineEnabled && MacOSFlags.isOGLVerbose()) {
            System.out.println("OpenGL pipeline enabled on screen " + displayID);
        }

        // initializes default device state, might be redundant step since we
        // call "displayChanged()" later anyway, but we do not want to leave the
        // device in an inconsistent state after construction
        displayChanged();
    }

    /**
     * Return a list of all configurations.
     */
    @Override
    public GraphicsConfiguration[] getConfigurations() {
        return new GraphicsConfiguration[]{config};
    }

    /**
     * Return the default configuration.
     */
    @Override
    public GraphicsConfiguration getDefaultConfiguration() {
        return config;
    }

    /**
     * Return a human-readable screen description.
     */
    @Override
    public String getIDstring() {
        return "Display " + displayID;
    }

    /**
     * Returns the type of the graphics device.
     * @see #TYPE_RASTER_SCREEN
     * @see #TYPE_PRINTER
     * @see #TYPE_IMAGE_BUFFER
     */
    @Override
    public int getType() {
        return TYPE_RASTER_SCREEN;
    }

    public double getXResolution() {
        return xResolution;
    }

    public double getYResolution() {
        return yResolution;
    }

    Rectangle getBounds() {
        return bounds.getBounds();
    }

    public Insets getScreenInsets() {
        // the insets are queried synchronously and are not cached
        // since there are no Quartz or Cocoa means to receive notifications
        // on insets changes (e.g. when the Dock is resized):
        // the existing CGDisplayReconfigurationCallBack is not notified
        // as well as the NSApplicationDidChangeScreenParametersNotification
        // is fired on the Dock location changes only
        return nativeGetScreenInsets(displayID);
    }

    public int getScaleFactor() {
        return scale;
    }

    /**
     * Invalidates this device so it will point to some other "new" device.
     *
     * @param  device the new device, usually the main screen
     */
    public void invalidate(CGraphicsDevice device) {
        //TODO do we need to restore the full-screen window/modes on old device?
        displayID = device.displayID;
        initialMode = device.initialMode;
    }

    @Override
    public void displayChanged() {
        xResolution = nativeGetXResolution(displayID);
        yResolution = nativeGetYResolution(displayID);
        bounds = nativeGetBounds(displayID).getBounds(); //does integer rounding
        initScaleFactor();
        resizeFSWindow(getFullScreenWindow(), bounds);
        //TODO configs?
    }

    @Override
    public void paletteChanged() {
        // devices do not need to react to this event.
    }

    /**
     * Enters full-screen mode, or returns to windowed mode.
     */
    @Override
    public synchronized void setFullScreenWindow(Window w) {
        Window old = getFullScreenWindow();
        if (w == old) {
            return;
        }

        boolean fsSupported = isFullScreenSupported();

        if (fsSupported && old != null) {
            // enter windowed mode and restore original display mode
            exitFullScreenExclusive(old);
            if (originalMode != null) {
                setDisplayMode(originalMode);
                originalMode = null;
            }
        }

        super.setFullScreenWindow(w);

        if (fsSupported && w != null) {
            if (isDisplayChangeSupported()) {
                originalMode = getDisplayMode();
            }
            // enter fullscreen mode
            enterFullScreenExclusive(w);
        }
    }

    /**
     * Returns true if this GraphicsDevice supports
     * full-screen exclusive mode and false otherwise.
     */
    @Override
    public boolean isFullScreenSupported() {
        return true;
    }

    private static void enterFullScreenExclusive(Window w) {
        FullScreenCapable peer = AWTAccessor.getComponentAccessor().getPeer(w);
        if (peer != null) {
            peer.enterFullScreenMode();
        }
    }

    private static void exitFullScreenExclusive(Window w) {
        FullScreenCapable peer = AWTAccessor.getComponentAccessor().getPeer(w);
        if (peer != null) {
            peer.exitFullScreenMode();
        }
    }

    /**
     * Reapplies the size of this device to the full-screen window.
     */
    private static void resizeFSWindow(final Window w, final Rectangle b) {
        if (w != null) {
            WindowPeer peer = AWTAccessor.getComponentAccessor().getPeer(w);
            if (peer != null) {
                peer.setBounds(b.x, b.y, b.width, b.height, SET_BOUNDS);
            }
        }
    }

    @Override
    public boolean isDisplayChangeSupported() {
        return true;
    }

    /* If the modes are the same or the only difference is that
     * the new mode will match any refresh rate, no need to change.
     */
    private boolean isSameMode(final DisplayMode newMode,
                               final DisplayMode oldMode) {

        return (Objects.equals(newMode, oldMode) ||
                (newMode.getRefreshRate() == DisplayMode.REFRESH_RATE_UNKNOWN &&
                 newMode.getWidth() == oldMode.getWidth() &&
                 newMode.getHeight() == oldMode.getHeight() &&
                 newMode.getBitDepth() == oldMode.getBitDepth()));
    }

    @Override
    public void setDisplayMode(final DisplayMode dm) {
        if (dm == null) {
            throw new IllegalArgumentException("Invalid display mode");
        }
        if (!isSameMode(dm, getDisplayMode())) {
            try {
                nativeSetDisplayMode(displayID, dm.getWidth(), dm.getHeight(),
                                    dm.getBitDepth(), dm.getRefreshRate());
            } catch (Throwable t) {
                /* In some cases macOS doesn't report the initial mode
                 * in the list of supported modes.
                 * If trying to reset to that mode causes an exception
                 * try one more time to reset using a different API.
                 * This does not fix everything, such as it doesn't make
                 * that mode reported and it restores all devices, but
                 * this seems a better compromise than failing to restore
                 */
                if (isSameMode(dm, initialMode)) {
                    nativeResetDisplayMode();
                    if (!isSameMode(initialMode, getDisplayMode())) {
                        throw new IllegalArgumentException(
                            "Could not reset to initial mode");
                    }
                } else {
                   throw t;
                }
            }
        }
    }

    @Override
    public DisplayMode getDisplayMode() {
        return nativeGetDisplayMode(displayID);
    }

    @Override
    public DisplayMode[] getDisplayModes() {
        DisplayMode[] nativeModes = nativeGetDisplayModes(displayID);
        boolean match = false;
        for (DisplayMode mode : nativeModes) {
            if (initialMode.equals(mode)) {
                match = true;
                break;
            }
        }
        if (match) {
            return nativeModes;
        } else {
          int len = nativeModes.length;
          DisplayMode[] modes = Arrays.copyOf(nativeModes, len+1, DisplayMode[].class);
          modes[len] = initialMode;
          return modes;
        }
    }

    public static boolean usingMetalPipeline() {
        return metalPipelineEnabled;
    }

    private void initScaleFactor() {
        if (SunGraphicsEnvironment.isUIScaleEnabled()) {
            double debugScale = SunGraphicsEnvironment.getDebugScale();
            scale = (int) (debugScale >= 1
                    ? Math.round(debugScale)
                    : nativeGetScaleFactor(displayID));
        } else {
            scale = 1;
        }
    }

    private static native double nativeGetScaleFactor(int displayID);

    private static native void nativeResetDisplayMode();

    private static native void nativeSetDisplayMode(int displayID, int w, int h, int bpp, int refrate);

    private static native DisplayMode nativeGetDisplayMode(int displayID);

    private static native DisplayMode[] nativeGetDisplayModes(int displayID);

    private static native double nativeGetXResolution(int displayID);

    private static native double nativeGetYResolution(int displayID);

    private static native Insets nativeGetScreenInsets(int displayID);

    private static native Rectangle2D nativeGetBounds(int displayID);
}
