package dev.webfx.kit.launcher.spi.impl.gwtj2cl;

import com.sun.javafx.application.ParametersImpl;
import dev.webfx.kit.launcher.spi.FastPixelReaderWriter;
import dev.webfx.kit.launcher.spi.impl.base.WebFxKitLauncherProviderBase;
import dev.webfx.kit.mapper.WebFxKitMapper;
import dev.webfx.kit.mapper.peers.javafxgraphics.NodePeer;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html.CanvasElementHelper;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html.Context2DHelper;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html.HtmlNodePeer;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html.UserInteraction;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.DragboardDataTransferHolder;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlFonts;
import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlUtil;
import dev.webfx.kit.util.properties.FXProperties;
import dev.webfx.platform.console.Console;
import dev.webfx.platform.uischeduler.UiScheduler;
import dev.webfx.platform.useragent.UserAgent;
import dev.webfx.platform.util.Numbers;
import dev.webfx.platform.util.Strings;
import dev.webfx.platform.util.collection.Collections;
import dev.webfx.platform.util.function.Factory;
import elemental2.dom.*;
import javafx.application.Application;
import javafx.application.HostServices;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.input.DataFormat;
import javafx.scene.input.Dragboard;
import javafx.scene.text.Font;
import javafx.stage.Screen;
import jsinterop.base.Js;
import jsinterop.base.JsPropertyMap;

import java.util.Map;


/**
 * @author Bruno Salmon
 */
public final class GwtJ2clWebFxKitLauncherProvider extends WebFxKitLauncherProviderBase {

    private Application application;
    private HostServices hostServices;

    public GwtJ2clWebFxKitLauncherProvider() {
        super(false);
    }

    @Override
    public HostServices getHostServices() {
        if (hostServices == null)
            hostServices = uri -> {
                // Note: Safari is blocking (on macOS) or ignoring (on iOS) window.open() when not called during a user
                // interaction. If we are in that case, it's better to postpone the window opening to the next user
                // interaction (which we hope will happen soon, such as a key or mouse release).
                if (UserAgent.isSafari() && !UserInteraction.isUserInteracting()) {
                    UserInteraction.runOnNextUserInteraction(() ->
                            DomGlobal.window.open(uri, "_blank")
                            , true);
                } else {
                    // For other browsers, or with Safari but during a user interaction (ex: mouse click), it's ok to
                    // open the browser window straightaway.
                    DomGlobal.window.open(uri, "_blank");
                }
            };
        return hostServices;
    }

    @Override
    public javafx.scene.input.Clipboard getSystemClipboard() {
        return new javafx.scene.input.Clipboard() {
            @Override
            public boolean setContent(Map<DataFormat, Object> content) {
                setClipboardContent((String) content.get(DataFormat.PLAIN_TEXT));
                super.setContent(content);
                return true;
            }

            @Override
            public Object getContentImpl(DataFormat dataFormat) {
                if (dataFormat == DataFormat.PLAIN_TEXT)
                    return getClipboardContent();
                return super.getContent(dataFormat);
            }
        };
    }

    private static void setClipboardContent(String text) {
        Clipboard.writeText(text);
    }

    private static String getClipboardContent() {
        String[] content = { null };
        Clipboard.readText().then(text -> {
            content[0] = text;
            return null;
        });
        return content[0]; // Assuming the promise was instantly fulfilled
    }

    @Override
    public Dragboard createDragboard(Scene scene) {
        return new Dragboard(scene) {
            @Override
            public boolean setContent(Map<DataFormat, Object> content) {
                boolean result = false;
                DataTransfer dragBoardDataTransfer = DragboardDataTransferHolder.getDragboardDataTransfer();
                dragBoardDataTransfer.clearData();
                if (content != null)
                    for (Map.Entry<DataFormat, Object> entry : content.entrySet()) {
                        String value = Strings.toString(entry.getValue());
                        for (String formatIdentifier : entry.getKey().getIdentifiers())
                            if (dragBoardDataTransfer.setData(formatIdentifier, value))
                                result = true;
                    }
                return result;
            }

            @Override
            public Object getContentImpl(DataFormat dataFormat) {
                DataTransfer dataTransfer = DragboardDataTransferHolder.getDragboardDataTransfer();
                if (dataFormat == DataFormat.FILES)
                    return dataTransfer.files;
                String jsType = Collections.first(dataFormat.getIdentifiers());
                return dataTransfer.getData(jsType);
            }
        };
    }

    @Override
    public Screen getPrimaryScreen() {
        elemental2.dom.Screen screen = DomGlobal.screen;
        return Screen.from(toRectangle2D(screen.width, screen.height), toRectangle2D(screen.availWidth, screen.availHeight), DomGlobal.window.devicePixelRatio, DomGlobal.window.devicePixelRatio);
    }

    private static Rectangle2D toRectangle2D(double width, double height) {
        return new Rectangle2D(0, 0, width, height);
    }

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

    @Override
    public boolean supportsWebPImageFormat() {
        return supportsWebPJS();
    }

    @Override
    public FastPixelReaderWriter getFastPixelReaderWriter(Image image) {
        return new GwtJ2clFastPixelReaderWriter(image);
    }

    @Override
    public GraphicsContext getGraphicsContext2D(Canvas canvas, boolean willReadFrequently) {
        return WebFxKitMapper.getGraphicsContext2D(canvas, willReadFrequently);
    }

    // HDPI management

    @Override
    public DoubleProperty canvasPixelDensityProperty(Canvas canvas) {
        String key = "webfx-canvasPixelDensityProperty";
        DoubleProperty canvasPixelDensityProperty = (DoubleProperty) canvas.getProperties().get(key);
        if (canvasPixelDensityProperty == null) {
            canvas.getProperties().put(key, canvasPixelDensityProperty = new SimpleDoubleProperty(getDefaultCanvasPixelDensity()));
            // Applying an immediate mapping between the JavaFX and HTML canvas, otherwise the default behaviour of the
            // WebFX mapper (which is to postpone and process the mapping in the next animation frame) wouldn't work for
            // canvas. The application will indeed probably draw in the canvas just after it is initialized (and sized).
            // If we were to wait for the mapper to resize the canvas in the next animation frame, it would be too late.
            HTMLCanvasElement canvasElement = (HTMLCanvasElement) ((HtmlNodePeer) canvas.getOrCreateAndBindNodePeer()).getElement();
            FXProperties.runNowAndOnPropertiesChange(() ->
                            CanvasElementHelper.resizeCanvasElement(canvasElement, canvas),
                    canvas.widthProperty(), canvas.heightProperty(), canvasPixelDensityProperty);

        }
        return canvasPixelDensityProperty;
    }

    private final HTMLCanvasElement MEASURE_CANVAS_ELEMENT = HtmlUtil.createElement("canvas");

    @Override
    public Bounds measureText(String text, Font font) {
        CanvasRenderingContext2D context = Context2DHelper.getCanvasContext2D(MEASURE_CANVAS_ELEMENT);
        context.font = HtmlFonts.getHtmlFontDefinition(font);
        TextMetrics textMetrics = context.measureText(text);
        JsPropertyMap<?> tm = Js.asPropertyMap(textMetrics);
        return new BoundingBox(0, 0, textMetrics.width, (double) tm.get("actualBoundingBoxAscent") + (double) tm.get("actualBoundingBoxDescent"));
    }

    private static final HTMLElement baselineSample = HtmlUtil.createSpanElement();
    private static final HTMLElement baselineLocator = HtmlUtil.createImageElement();
    static {
        baselineSample.appendChild(DomGlobal.document.createTextNode("Baseline text"));
        baselineSample.style.position = "absolute";
        baselineSample.style.lineHeight = CSSProperties.LineHeightUnionType.of("100%");
        baselineLocator.style.verticalAlign = "baseline";
        baselineSample.appendChild(baselineLocator);
    }

    @Override
    public double measureBaselineOffset(Font font) {
        if (font == null)
            return 0;
        if (!font.isBaselineOffsetSet()) {
            HtmlFonts.setHtmlFontStyleAttributes(font, baselineSample);
            DomGlobal.document.body.appendChild(baselineSample);
            double top = baselineSample.getBoundingClientRect().top;
            double baseLineY = baselineLocator.getBoundingClientRect().bottom;
            DomGlobal.document.body.removeChild(baselineSample);
            double baselineOffset = baseLineY - top;
            font.setBaselineOffset(baselineOffset);
        }
        return font.getBaselineOffset();
    }

    @Override
    public ObservableList<Font> loadingFonts() {
        return Font.getLoadingFonts();
    }

    private static boolean supportsWebPJS() {
        // Check FF, Edge by user agent
        if (UserAgent.isFireFox())
            return UserAgent.getBrowserMajorVersion() >= 65;
        if (UserAgent.isEdge())
            return UserAgent.getBrowserMajorVersion() >= 18;
        // Use canvas hack for webkit-based browsers
        HTMLCanvasElement e = (HTMLCanvasElement) DomGlobal.document.createElement("canvas");
        return Js.asPropertyMap(e).has("toDataURL") && e.toDataURL("image/webp").startsWith("data:image/webp");
    };

    @Override
    public void launchApplication(Factory<Application> applicationFactory, String... args) {
        application = applicationFactory.create();
        if (application != null)
            try {
                ParametersImpl.registerParameters(application, new ParametersImpl(args));
                application.init();
                application.start(getPrimaryStage());
            } catch (Exception e) {
                Console.log("Error while launching the JavaFX application", e);
            }
    }

    @Override
    public Application getApplication() {
        return application;
    }

    private ObjectProperty<Insets> safeAreaInsetsProperty = null;

    @Override
    public ReadOnlyObjectProperty<Insets> safeAreaInsetsProperty() {
        if (safeAreaInsetsProperty == null) {
            safeAreaInsetsProperty = new SimpleObjectProperty<>(Insets.EMPTY);
            FXProperties.runNowAndOnPropertiesChange(this::updateSafeAreaInsets,
                getPrimaryStage().widthProperty(), getPrimaryStage().heightProperty());
            // Workaround for a bug observed in the Gmail internal browser on iPad where the window width/height
            // are still not final at the first opening. So we schedule a subsequent update to get final values.
            UiScheduler.scheduleDelay(500, this::updateSafeAreaInsets); // 500ms seem enough
        }
        return safeAreaInsetsProperty;
    }

    public void updateSafeAreaInsets() {
        /* The following code is relying on this CSS rule present in webfx-kit-javafxgraphics-web@main.css
        :root {
            --safe-area-inset-top:    env(safe-area-inset-top);
            --safe-area-inset-right:  env(safe-area-inset-right);
            --safe-area-inset-bottom: env(safe-area-inset-bottom);
            --safe-area-inset-left:   env(safe-area-inset-left);
        }
         */
        CSSStyleDeclaration computedStyle = Js.<ViewCSS>uncheckedCast(DomGlobal.window).getComputedStyle(DomGlobal.document.documentElement);
        String top    = computedStyle.getPropertyValue("--safe-area-inset-top");
        String right  = computedStyle.getPropertyValue("--safe-area-inset-right");
        String bottom = computedStyle.getPropertyValue("--safe-area-inset-bottom");
        String left   = computedStyle.getPropertyValue("--safe-area-inset-left");
        safeAreaInsetsProperty.set(new Insets(
            Numbers.doubleValue(Strings.removeSuffix(top, "px")),
            Numbers.doubleValue(Strings.removeSuffix(right, "px")),
            Numbers.doubleValue(Strings.removeSuffix(bottom, "px")),
            Numbers.doubleValue(Strings.removeSuffix(left, "px"))
        ));
    }

    @Override
    public boolean isFullscreenEnabled() {
        return DomGlobal.document.fullscreenEnabled;
    }

    @Override
    public boolean requestNodeFullscreen(Node node) {
        if (!isFullscreenEnabled())
            return false;
        NodePeer nodePeer = node == null ? null : node.getOrCreateAndBindNodePeer();
        if (nodePeer instanceof HtmlNodePeer) {
            ((HtmlNodePeer) node.getNodePeer()).getElement().requestFullscreen();
            return true;
        }
        return false;
    }

    @Override
    public boolean exitFullscreen() {
        if (DomGlobal.document.fullscreenElement == null && !getPrimaryStage().isFullScreen())
            return false;
        DomGlobal.document.exitFullscreen();
        return true;
    }

    {
        DomGlobal.document.addEventListener("fullscreenchange", e ->
            getPrimaryStage().fullScreenPropertyImpl().set(DomGlobal.document.fullscreenElement != null)
        );
    }
}