package p.studio.window;

import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import p.packer.messages.InitWorkspaceRequest;
import p.studio.Container;
import p.studio.lsp.events.*;
import p.studio.projects.KnownProjectsService;
import p.studio.projects.ProjectCatalogService;
import p.studio.projects.ProjectReference;
import p.studio.projectsessions.StudioProjectSession;
import p.studio.projectsessions.StudioProjectSessionFactory;
import p.studio.utilities.i18n.I18n;

import java.nio.file.Path;
import java.util.Objects;

public final class StudioWindowCoordinator {
    private static final double LAUNCHER_WIDTH = 760;
    private static final double LAUNCHER_HEIGHT = 750;
    private static final double PROJECT_LOADING_WIDTH = 460;
    private static final double PROJECT_LOADING_HEIGHT = 250;
    private static final double PROJECT_WIDTH = 1280;
    private static final double PROJECT_HEIGHT = 840;

    private final Stage launcherStage;
    private final ProjectCatalogService projectCatalogService;
    private final KnownProjectsService knownProjectsService;
    private final WindowStateService windowStateService;
    private final ProjectLauncherView launcherView;
    private final StudioProjectSessionFactory projectSessionFactory;

    public StudioWindowCoordinator(Stage launcherStage) {
        this.launcherStage = Objects.requireNonNull(launcherStage, "launcherStage");
        this.projectCatalogService = new ProjectCatalogService(resolveDefaultProjectsRoot());
        this.knownProjectsService = new KnownProjectsService(resolveKnownProjectsStorage(), projectCatalogService);
        this.windowStateService = new WindowStateService(resolveWindowStateStorage());
        this.projectSessionFactory = new StudioProjectSessionFactory(Container.lspServerLifecycle());
        this.launcherView = new ProjectLauncherView(
                knownProjectsService,
                projectCatalogService,
                this::openProject,
                this::addProject,
                this::createProject,
                this::forgetProject);
    }

    public void showLauncher() {
        final Scene scene = new Scene(launcherView, LAUNCHER_WIDTH, LAUNCHER_HEIGHT);
        scene.getStylesheets().add(Container.theme().getDefaultTheme());

        launcherStage.titleProperty().bind(Container.i18n().bind(I18n.APP_TITLE));
        launcherStage.setScene(scene);
        windowStateService.installLauncherState(launcherStage, LAUNCHER_WIDTH, LAUNCHER_HEIGHT);
        launcherStage.setResizable(false);
        launcherStage.setWidth(LAUNCHER_WIDTH);
        launcherStage.setHeight(LAUNCHER_HEIGHT);
        launcherStage.setMinWidth(LAUNCHER_WIDTH);
        launcherStage.setMaxWidth(LAUNCHER_WIDTH);
        launcherStage.setMinHeight(LAUNCHER_HEIGHT);
        launcherStage.setMaxHeight(LAUNCHER_HEIGHT);
        launcherStage.show();
        launcherStage.centerOnScreen();
        launcherStage.toFront();
    }

    private void openProject(ProjectReference projectReference) {
        final ProjectReference opened = projectCatalogService.openProject(projectReference.rootPath());
        knownProjectsService.remember(opened);
        Container.eventBus().publish(new StudioProjectOpenedEvent());
        openProjectWindow(opened);
    }

    private void createProject(ProjectReference projectReference) {
        knownProjectsService.remember(projectReference);
        Container.eventBus().publish(new StudioProjectCreatedEvent());
        Container.eventBus().publish(new StudioProjectOpenedEvent());
        openProjectWindow(projectReference);
    }

    private void addProject(ProjectReference projectReference) {
        knownProjectsService.remember(projectReference);
        launcherView.reloadProjects();
    }

    private void forgetProject(ProjectReference projectReference) {
        knownProjectsService.forget(projectReference);
        launcherView.reloadProjects();
    }

    private void openProjectWindow(ProjectReference projectReference) {
        final Stage loadingStage = createProjectLoadingStage(projectReference);
        launcherStage.hide();
        loadingStage.show();
        loadingStage.centerOnScreen();
        loadingStage.toFront();
        publishProjectLoadingStarted(projectReference);
        Container.backgroundTasks().submit(() -> initializeProjectAndOpenWindow(projectReference, loadingStage));
    }

    private Stage createProjectLoadingStage(ProjectReference projectReference) {
        final Stage loadingStage = new Stage(StageStyle.UNDECORATED);
        loadingStage.initOwner(launcherStage);
        loadingStage.setResizable(false);

        final Scene scene = new Scene(
                new ProjectLoadingShieldView(projectReference),
                PROJECT_LOADING_WIDTH,
                PROJECT_LOADING_HEIGHT);
        scene.getStylesheets().add(Container.theme().getDefaultTheme());
        loadingStage.setScene(scene);
        return loadingStage;
    }

    private void initializeProjectAndOpenWindow(ProjectReference projectReference, Stage loadingStage) {
        StudioProjectSession projectSession = null;
        try {
            Container.eventBus().publish(new StudioProjectLoadingProgressEvent(
                    StudioProjectLoadingPhase.INITIALIZING_SERVICES,
                    Container.i18n().text(I18n.SHIELD_STATUS_INDEXING),
                    Progress.INITIALIZING_SERVICES,
                    false));
            Container.packer()
                    .workspaceService()
                    .initWorkspace(new InitWorkspaceRequest(projectReference.toPackerProjectContext()));
            Container.eventBus().publish(new StudioProjectLoadingProgressEvent(
                    StudioProjectLoadingPhase.RESTORING_WORKSPACES,
                    Container.i18n().text(I18n.SHIELD_STATUS_RESTORING),
                    Progress.RESTORING_WORKSPACES,
                    false));
            projectSession = projectSessionFactory.open(projectReference);
            final StudioProjectSession readySession = projectSession;
            Platform.runLater(() -> finishProjectOpen(readySession, loadingStage));
        } catch (RuntimeException exception) {
            if (projectSession != null) {
                projectSession.close();
            }
            Platform.runLater(() -> failProjectOpen(projectReference, loadingStage, exception));
        }
    }

    private void finishProjectOpen(StudioProjectSession projectSession, Stage loadingStage) {
        final ProjectReference projectReference = projectSession.projectReference();
        final Stage projectStage = new Stage();
        final MainView mainView = new MainView(projectSession);
        final Scene scene = new Scene(mainView, PROJECT_WIDTH, PROJECT_HEIGHT);
        scene.getStylesheets().add(Container.theme().getDefaultTheme());

        projectStage.setTitle(Container.i18n().format(I18n.APP_PROJECT_TITLE, projectReference.name()));
        projectStage.setScene(scene);
        windowStateService.installProjectShellState(projectStage, projectReference, PROJECT_WIDTH, PROJECT_HEIGHT);
        projectStage.setOnHidden(ignored -> {
            mainView.close();
            projectSession.close();
            launcherView.reloadProjects();
            launcherStage.show();
            launcherStage.centerOnScreen();
            launcherStage.toFront();
        });

        Container.eventBus().publish(new StudioProjectLoadingProgressEvent(
                StudioProjectLoadingPhase.READY,
                Container.i18n().format(I18n.APP_PROJECT_TITLE, projectReference.name()),
                Progress.READY,
                false));
        Container.eventBus().publish(new StudioProjectLoadingCompletedEvent());
        loadingStage.close();
        projectStage.show();
        projectStage.toFront();
        Platform.runLater(mainView::reapplyProjectLocalLayout);
    }

    private void failProjectOpen(
            ProjectReference projectReference,
            Stage loadingStage,
            RuntimeException exception) {
        final String message = exception.getMessage() == null || exception.getMessage().isBlank()
                ? "Unable to initialize project services for " + projectReference.name() + "."
                : exception.getMessage();
        Container.eventBus().publish(new StudioProjectLoadingFailedEvent(
                StudioProjectLoadingPhase.INITIALIZING_SERVICES,
                message));
        loadingStage.close();
        launcherStage.show();
        launcherStage.centerOnScreen();
        launcherStage.toFront();
    }

    private void publishProjectLoadingStarted(ProjectReference projectReference) {
        Container.eventBus().publish(new StudioProjectLoadingStartedEvent());
        Container.eventBus().publish(new StudioProjectLoadingProgressEvent(
                StudioProjectLoadingPhase.RESOLVING_PROJECT,
                Container.i18n().format(I18n.SHIELD_LOADING_PROJECT, projectReference.name()),
                Progress.RESOLVING_PROJECT,
                false));
    }

    private static final class Progress {
        private static final double RESOLVING_PROJECT = 0.20d;
        private static final double INITIALIZING_SERVICES = 0.58d;
        private static final double RESTORING_WORKSPACES = 0.86d;
        private static final double READY = 1.0d;

        private Progress() {
        }
    }

    private Path resolveDefaultProjectsRoot() {
        Path cursor = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
        while (cursor != null) {
            if (cursor.resolve("settings.gradle.kts").toFile().exists()
                    && cursor.resolve("test-projects").toFile().exists()) {
                return cursor.resolve("test-projects");
            }
            cursor = cursor.getParent();
        }
        return Path.of("test-projects").toAbsolutePath().normalize();
    }

    private Path resolveKnownProjectsStorage() {
        return Path.of(System.getProperty("user.home"), ".prometeu-studio", "known-projects.txt")
                .toAbsolutePath()
                .normalize();
    }

    private Path resolveWindowStateStorage() {
        return Path.of(System.getProperty("user.home"), ".prometeu-studio", "window-state.json")
                .toAbsolutePath()
                .normalize();
    }
}
