package space.fediverse.graph;

import org.gephi.graph.api.GraphController;
import org.gephi.graph.api.GraphModel;
import org.gephi.graph.api.Node;
import org.gephi.graph.api.UndirectedGraph;
import org.gephi.io.database.drivers.PostgreSQLDriver;
import org.gephi.io.database.drivers.SQLUtils;
import org.gephi.io.exporter.api.ExportController;
import org.gephi.io.importer.api.Container;
import org.gephi.io.importer.api.EdgeDirectionDefault;
import org.gephi.io.importer.api.ImportController;
import org.gephi.io.importer.plugin.database.EdgeListDatabaseImpl;
import org.gephi.io.importer.plugin.database.ImporterEdgeList;
import org.gephi.io.processor.plugin.DefaultProcessor;
import org.gephi.layout.plugin.AutoLayout;
import org.gephi.layout.plugin.forceAtlas2.ForceAtlas2;
import org.gephi.project.api.ProjectController;
import org.gephi.project.api.Workspace;
import org.openide.util.Lookup;

import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.concurrent.TimeUnit;

public class GraphBuilder {
    private static final String nodeQuery = new StringBuilder().append("SELECT i.domain as id, i.domain as label")
            .append(" FROM instances i INNER JOIN edges e ON i.domain = e.source_domain OR i.domain = e.target_domain")
            .append(" WHERE i.user_count IS NOT NULL AND NOT i.opt_out AND i.type IS NOT NULL").toString();

    private static final String edgeQuery = new StringBuilder().append("SELECT e.source_domain AS source,")
            .append(" e.target_domain AS target, e.weight AS weight FROM edges e").toString();

    public static void main(String[] args) {
        // System.out.println("Node query: " + nodeQuery);
        // System.out.println("Edge query: " + edgeQuery);

        // Init project & workspace; required to do things w/ gephi
        ProjectController pc = Lookup.getDefault().lookup(ProjectController.class);
        pc.newProject();
        Workspace workspace = pc.getCurrentWorkspace();

        // Get controllers and models
        ImportController importController = Lookup.getDefault().lookup(ImportController.class);
        GraphModel graphModel = Lookup.getDefault().lookup(GraphController.class).getGraphModel();
        // AttributeModel?

        // Get config variables

        // DATABASE_URL has the format postgres://user:password@host:port/database
        String[] databaseParams = System.getenv("DATABASE_URL").replace("postgres://", "").split(":|@|/");
        String postgresUser = databaseParams[0];
        String postgresPassword = databaseParams[1];
        String postgresHost = databaseParams[2];
        Integer postgresPort = Integer.parseInt(databaseParams[3]);
        String postgresDb = databaseParams[4];
        if (postgresUser == null || postgresPassword == null || postgresHost == null || postgresPort == null
                || postgresDb == null) {
            throw new RuntimeException(
                    String.format("Incomplete config, canceling. DB: %s, user: %s, host: %s, port: %s", postgresDb,
                            postgresUser, postgresHost, postgresPort));
        }

        // Import from database
        EdgeListDatabaseImpl db = new EdgeListDatabaseImpl();
        db.setSQLDriver(new PostgreSQLDriver());
        db.setUsername(postgresUser);
        db.setPasswd(postgresPassword);
        db.setHost(postgresHost);
        db.setPort(postgresPort);
        db.setDBName(postgresDb);
        db.setNodeQuery(nodeQuery);
        db.setEdgeQuery(edgeQuery);

        ImporterEdgeList edgeListImporter = new ImporterEdgeList();
        Container container = importController.importDatabase(db, edgeListImporter);
        // If a node is in the edge list, but not node list, we don't want to create it
        // automatically
        container.getLoader().setAllowAutoNode(false);
        container.getLoader().setAllowSelfLoop(false);
        container.getLoader().setEdgeDefault(EdgeDirectionDefault.UNDIRECTED); // This is an undirected graph

        // Add imported data to graph
        importController.process(container, new DefaultProcessor(), workspace);

        // Layout
        AutoLayout autoLayout = new AutoLayout(2, TimeUnit.MINUTES);
        autoLayout.setGraphModel(graphModel);
        // YifanHuLayout firstLayout = new YifanHuLayout(null, new
        // StepDisplacement(1f));
        ForceAtlas2 forceAtlas2Layout = new ForceAtlas2(null);
        forceAtlas2Layout.setLinLogMode(true);
        autoLayout.addLayout(forceAtlas2Layout, 1f);
        autoLayout.execute();

        // Update coordinates in database
        // First, connect
        String dbUrl = SQLUtils.getUrl(db.getSQLDriver(), db.getHost(), db.getPort(), db.getDBName());
        Connection conn = null;
        try {
            conn = db.getSQLDriver().getConnection(dbUrl, db.getUsername(), db.getPasswd());
        } catch (SQLException e) {
            if (conn != null) {
                try {
                    conn.close();
                } catch (Exception e2) {
                    // Closing failed; ah well
                }
            }
            throw new RuntimeException(e);
        }
        // Remove all x and y
        try {
            PreparedStatement delStatement = conn.prepareStatement("UPDATE instances SET x=NULL, y=NULL");
            delStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        // Update to new x's and y's
        UndirectedGraph graph = graphModel.getUndirectedGraph();
        for (Node node : graph.getNodes()) {
            String id = node.getId().toString();
            float x = node.x();
            float y = node.y();

            try {
                PreparedStatement statement = conn.prepareStatement("UPDATE instances SET x=?, y=? WHERE domain=?");
                statement.setFloat(1, x);
                statement.setFloat(2, y);
                statement.setString(3, id);
                statement.executeUpdate();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        // Close connection
        try {
            conn.close();
        } catch (SQLException e) {
            // Closing failed; ah well
        }

        // Also export to gexf
        ExportController exportController = Lookup.getDefault().lookup(ExportController.class);
        try {
            exportController.exportFile(new File("fediverse.gexf"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // Gephi doesn't seem to provide a good way to close its postgres connection, so
        // we have to force close the
        // program. This'll leave a hanging connection for some period ¯\_(ツ)_/¯
        System.exit(0);
    }
}
