/**
 * This class provides utilities for reading and processing point data, control points, 
 * and performing partitioning operations using Octree structures.
 * It includes methods for file parsing, Euclidean distance calculations, 
 * partition operations, and formatting of nested data structures.
 *
 * @author Christopher Dedman-Rollet
 * @date   11/23/2024
 */

package src;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class CGPartitioning
{
    /**
     * Reads a file containing points with associated charge values and parses the data.
     * 
     * @param filePath the path to the file containing points with charges.
     * @return a list of PointWithCharge objects representing the parsed data.
     */
    public static List<PointWithCharge> readPtsFileWithCharge(String filePath)
    {
        List<PointWithCharge> dataList = new ArrayList<>();
        try (BufferedReader buffer = new BufferedReader(new FileReader(filePath)))
        {
            String line;
            while ((line = buffer.readLine()) != null)
            {
                // Use regex to remove curly brackets from the input file
                String output   = line.replaceAll("[{}]", "");
                // Split the line by comma and whitespace
                String[] tokens = output.split(",\\s*");

                // Extract coordinates (first 3 elements) and charge (last element)
                double[] coordinates = new double[3];
                for (int i = 0; i < 3; i++)
                {
                    coordinates[i] = Double.parseDouble(tokens[i]);
                }
                double charge = Double.parseDouble(tokens[3]);

                // Create a PointWithCharge object and add it to the list
                dataList.add(new PointWithCharge(coordinates, charge));
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }

        return dataList;
    }

    /**
     * Reads a file containing control points and parses the data into a 2D array of doubles.
     * 
     * @param filePath the path to the file containing control points.
     * @return a 2D array of doubles representing the control points.
     */
    public static double[][] readCntrlPointFile(String filePath)
    {
        List<double[]> dataList     = new ArrayList<>();
        try (BufferedReader buffer  = new BufferedReader(new FileReader(filePath)))
        {
            String line;
            while ((line = buffer.readLine()) != null)
            {
                // Use regex to remove curly brackets from the input file
                String output   = line.replaceAll("[{}]", "");
                // Split the line by comma and whitespace
                String[] tokens = output.split(",\\s*");
                double[] row    = new double[tokens.length];

                // Parse each token as a double
                for (int i = 0; i < tokens.length; i++)
                {
                    row[i] = Double.parseDouble(tokens[i]);
                }
                dataList.add(row);
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }

        return dataList.toArray(new double[0][]);
    }

    /**
     * Calculates the Euclidean distance between two points in n-dimensional space.
     * 
     * @param point1 the first point as an array of doubles.
     * @param point2 the second point as an array of doubles.
     * @return the Euclidean distance between the two points.
     */
    private static double euclideanDistance(double[] point1, double[] point2)
    {
        double sum = 0.0;
        for (int i = 0; i < point1.length; i++)
        {
            sum += Math.pow(point1[i] - point2[i], 2);
        }
        return Math.sqrt(sum);
    }

    /**
     * Checks whether all points in one partition element are within the bounds of another.
     * 
     * @param partitionElement1 the first partition element containing points.
     * @param partitionElement2 the second partition element with a center and radius.
     * @return true if all points in partitionElement1 are within partitionElement2's bounds; otherwise, false.
     */
    public static boolean isIn(Object[] partitionElement1, Object[] partitionElement2)
    {
        // Extract center (cnt2) and radius (rad2) from partitionElement2
        double[] cnt2 = (double[]) ((Object[]) ((Object[]) partitionElement2[0])[0])[0];
        double rad2   = (double) partitionElement2[2];

        // Extract points from partitionElement1
        Object[] points1 = (Object[]) partitionElement1[0];

        // Iterate through each point in partitionElement1
        for (Object pointObj : points1)
        {
            double[] pnt1 = (double[]) ((Object[]) pointObj)[0];

            // Check Euclidean distance
            if (euclideanDistance(pnt1, cnt2) > rad2)
            {
                return false;
            }
        }
        return true;
    }

    /**
     * Merges two partition elements by combining their points.
     * 
     * @param partitionElement1 the first partition element.
     * @param partitionElement2 the second partition element.
     * @return a merged partition element.
     */
    public static Object[] mergeIn(Object[] partitionElement1, Object[] partitionElement2)
    {
        // Create a copy of partitionElement2 to modify
        Object[] merged  = partitionElement2.clone();
        // Extract points from both partition elements
        Object[] points1 = (Object[]) partitionElement1[0];
        Object[] points2 = (Object[]) partitionElement2[0];

        // Combine points from partitionElement1 and partitionElement2
        List<Object> combinedPoints = new ArrayList<>();
        for (Object point : points2)
        {
            combinedPoints.add(point);
        }

        for (Object point : points1)
        {
            combinedPoints.add(point);
        }

        // Update the merged partition's points
        merged[0] = combinedPoints.toArray(new Object[0]);

        return merged;
    }

    /**
     * Generates a coarse-grained partition from an initial fine-grained partition using iterative merging.
     * 
     * @param inPartition the initial fine-grained partition as a list of partition elements.
     * @return a coarse-grained partition as a list of partition elements.
     */
    public static List<Object[]> generateCGPartition(List<Object[]> inPartition)
    {
        System.out.println("\nThe partition produced by the method is irreducible but not unique: " + //
                                "the result depends on the order of the elements in the initial partition");
        List<Object[]> wpart = new ArrayList<>(inPartition);
        if (wpart.size() == 1)
        {
            return wpart;
        }

        int steps = 0;
        int j     = 1;

        while (j < wpart.size())
        {
            int i  = j;
            int jm = j - 1;
            while (i < wpart.size())
            {
                steps++;
                if (isIn(wpart.get(jm), wpart.get(i)))
                {
                    wpart.set(i, mergeIn(wpart.get(jm), wpart.get(i)));
                    i = j;
                    wpart.remove(jm);
                }
                else if (isIn(wpart.get(i), wpart.get(jm)))
                {
                    wpart.set(jm, mergeIn(wpart.get(i), wpart.get(jm)));
                    wpart.remove(i);
                }
                else
                {
                    i++;
                }
            }
            j++;
        }

        System.out.println("\nNumber of steps: " + steps);

        return wpart;
    }

    /**
     * Formats an object into a string representation with nested curly braces.
     * 
     * @param obj the object to format.
     * @return the formatted string representation of the object.
     */
    public static String formatWithCurlyBraces(Object obj)
    {
        // Check if the input object is an array of objects (Object[]).
        if (obj instanceof Object[])
        {
            Object[] array   = (Object[]) obj;
            StringBuilder sb = new StringBuilder("{");

            // Iterate through the array elements and recursively format each elem
            for (int i = 0; i < array.length; i++)
            {
                sb.append(formatWithCurlyBraces(array[i]));
                if (i < array.length - 1)
                {
                    sb.append(", ");
                }
            }

            sb.append("}");
            return sb.toString();
        }
        // Check if the input object is an array of doubles (double[]).
        else if (obj instanceof double[])
        {
            double[] array   = (double[]) obj;
            StringBuilder sb = new StringBuilder("{");

            // Iterate through the double array elements.
            for (int i = 0; i < array.length; i++)
            {
                sb.append(array[i]);
                if (i < array.length - 1)
                {
                    sb.append(", ");
                }
            }

            sb.append("}");
            return sb.toString();
        }
        // For non-array objects, directly use their string representation 
        // (for example, index and distance).
        else
        {
            return obj.toString();
        }
    }

    /**
     * The main method serves as the entry point for executing the program.
     * It performs the following steps:
     *    1. Reads input points and control points from files.
     *    2. Builds an Octree structure based on the input data.
     *    3. Creates and processes partition data.
     *    4. Generates a coarse-grained partition and output the results into an output file.
     */
    public static void main(String[] args)
    {
        // Check if arguments are provided
        if (args.length == 0)
        {
            System.err.println("\nError: No command-line arguments provided.");
            System.err.println("Usage:\nmake ARGS='pointsFile.txt controlPointsFile.txt outputFile.txt'\n"
                    + "or manually run:\njava -cp build src.CGPartitioning pointsWithCharge1MYK.txt controlPoints1MYK.txt outputFile.txt\n");
            System.exit(1); // Exit with a non-zero status to indicate an error
        }

        String ptsData         = args[0]; // inputs point + charge file
        String cntrlPointsData = args[1]; // control point file
        String outputFile      = args[2]; // name of output file

        // Read control points and input points (with charge)
        double[][] cntrlPoints      = readCntrlPointFile(cntrlPointsData);
        List<PointWithCharge> cdata = readPtsFileWithCharge(ptsData);

        // Extract coordinates from cdata
        double[][] pts = new double[cdata.size()][3];
        for (int i = 0; i < cdata.size(); i++)
        {
            pts[i] = cdata.get(i).getCoordinates();
        }

        // // DEBUGGING: Print cdata full list
        // for (PointWithCharge obj : cdata)
        // {
        //     System.out.println(obj);
        // }

        double[] offset    = { 0.0, 0.0, 0.0 };
        int spaceDimension = 3;

        // Create and build the Octree
        Octree octree = new Octree(spaceDimension, pts, cntrlPoints, offset);
        try
        {
            octree.build();
        }
        catch (ExcessiveDivisionException e)
        {
            System.err.println("Error during octree build: " + e.getMessage());
        }

        System.out.println("\nOctree built successfully");
        System.out.println("Data read successfully. Partitioning...\n");

        int[][] pairs = octree.getPartition();

        // Create 'parts1MYK' list with the desired format
        List<Object[]> parts1MYK = new ArrayList<>();
        for (int i = 0; i < pairs.length; i++)
        {
            int index1 = pairs[i][0];
            int index2 = pairs[i][1];

            // Calculate Euclidean distance
            double distance = euclideanDistance(pts[index1], cntrlPoints[index2]);

            // Add nested structure: {{{coordinates, charge}}, index2, distance}
            parts1MYK.add(new Object[] {
                    new Object[] { new Object[] { cdata.get(index1).getCoordinates(), cdata.get(index1).getCharge() } },
                    index2 + 1, distance });
        }

        // TEST: print a single element of the 'parts' list
        if (!parts1MYK.isEmpty())
        {
            System.out.print("First element of parts: ");
            System.out.println(formatWithCurlyBraces(parts1MYK.get(0)));
        }

        // Print the full 'parts1MYK' list in the desired format
        // if (!parts1MYK.isEmpty())
        // {
        //     for (int i = 0; i < parts1MYK.size(); i++)
        //     {
        //         if (parts1MYK.get(i) instanceof Object[])
        //         {
        //             System.out.println(formatWithCurlyBraces(parts1MYK.get(i)));
        //         }
        //     }
        // }

        List<Object[]> irrPart1MYK = generateCGPartition(parts1MYK);

        // TEST: Print a single element of 'wpart' list
        if (!irrPart1MYK.isEmpty())
        {
            System.out.print("Second element of irrPart1MYK: ");
            System.out.println(formatWithCurlyBraces(irrPart1MYK.get(1)));
        }

        // Try block to check if exception occurs
        try
        {
            // Create a FileWriter object to write the output in a file
            FileWriter fWriter = new FileWriter(outputFile);

            if (!irrPart1MYK.isEmpty())
            {
                for (int i = 0; i < irrPart1MYK.size(); i++)
                {
                    if (irrPart1MYK.get(i) instanceof Object[])
                    {
                        fWriter.write(formatWithCurlyBraces(irrPart1MYK.get(i)));
                        fWriter.write('\n'); // this will have one output per line
                    }
                }
            }
            fWriter.close();

            System.out.println("\nFile is created successfully and data copied to the file.");
        }
        // Catch block to handle if exception occurs
        catch (IOException e)
        {
            System.err.println(e.getMessage());
        }

        System.out.println("\nLength of parts1MYK: " + parts1MYK.size());
        System.out.println("Length of irrPart1MYK: " + irrPart1MYK.size());
        System.out.println("\nDone!");
    }
}