package com.srbenoit.color;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Toolkit;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import javax.swing.JFrame;
import com.srbenoit.log.LoggedPanel;
import com.srbenoit.math.linear.DMatrix;

/**
 * Class to load several matrix files, determines the entire range of values, and creates a single
 * color gradient scale for all the images.
 */
public class ScaleUnifier extends LoggedPanel {

    /** version number for serialization */
    private static final long serialVersionUID = 7818251645492851675L;

    /** the list of loaded matrices */
    private final transient DMatrix[] matrices;

    /** screen locations at which to draw each matrix */
    private final transient Point[] locations;

    /** the color gradient */
    private final transient Gradient gradient;

    /** the maximum values for each color range */
    private final transient double[] range;

    /**
     * Constructs a new <code>ScaleUnifier</code>.
     *
     * @param   numColors  the number of colors in the scale
     * @param   numCycles  the number of cycles in the scale
     * @param   files      the list of matrix data files to process
     * @throws  IOException  if there is an error reading a matrix data file
     */
    public ScaleUnifier(final int numColors, final int numCycles, final File[] files)
        throws IOException {

        super();

        this.gradient = new Gradient(numColors, numCycles);
        this.range = new double[numColors];
        this.matrices = new DMatrix[files.length];
        this.locations = new Point[files.length];

        for (int i = 0; i < files.length; i++) {
            this.matrices[i] = DMatrix.load(files[i]);
        }
    }

    /**
     * Builds a single scale for the data.
     *
     * @param  logBase  the base with which to take logarithms (0 for no logs)
     */
    public void makeScale(final double logBase) {

        if (logBase < 0) {
            makeSmoothScale();
        } else if (logBase == 0) {
            makeLinearScale();
        } else {
            makeLogScale(logBase);
        }
    }

    /**
     * Builds a linear scale for the data.
     */
    private void makeSmoothScale() {

        int size = 0;
        int index = 0;
        DMatrix matrix;
        double[] ranges;

        // Form a combined matrix with all values from the given matrices
        for (DMatrix mat : this.matrices) {
            size += mat.numColumns() * mat.numRows();
        }

        matrix = new DMatrix(1, size);

        for (DMatrix mat : this.matrices) {

            for (int c = 0; c < mat.numColumns(); c++) {

                for (int r = 0; r < mat.numRows(); r++) {
                    matrix.set(0, index, mat.get(r, c));
                    index++;
                }
            }
        }

        // Have the matrix generate the gradient.
        ranges = matrix.colorRanges(this.range.length);
        System.arraycopy(ranges, 0, this.range, 0, this.range.length);
    }

    /**
     * Builds a linear scale for the data.
     */
    private void makeLinearScale() {

        int width;
        int height;
        double min;
        double max;
        double cur;

        // Compute the minimum/maximum values
        min = this.matrices[0].get(0, 0);
        max = min;

        for (int i = 0; i < this.matrices.length; i++) {
            width = this.matrices[i].numColumns();
            height = this.matrices[i].numRows();

            for (int c = 0; c < width; c++) {

                for (int r = 0; r < height; r++) {
                    cur = this.matrices[i].get(r, c);

                    if (cur < min) {
                        min = cur;
                    }

                    if (cur > max) {
                        max = cur;
                    }
                }
            }
        }

        LOG.log(Level.FINE, "Overall range: {0} to {1}", new Object[] { min, max });

        // Now, create a linear gradient of those values.
        for (int i = 0; i < this.range.length; i++) {
            this.range[i] = min + ((max - min) * i / this.range.length);
        }
    }

    /**
     * Builds a logarithmic scale for the data.
     *
     * @param  logBase  the base with which to take logarithms, using log_a(x) = log(x) / log(a)
     */
    private void makeLogScale(final double logBase) {

        double loga;
        int width;
        int height;
        double min;
        double max;
        double cur;

        loga = Math.log10(logBase);

        // Compute the minimum/maximum values
        cur = Math.log10(this.matrices[0].get(0, 0)) / loga;
        min = cur;
        max = cur;

        for (int i = 0; i < this.matrices.length; i++) {
            width = this.matrices[i].numColumns();
            height = this.matrices[i].numRows();

            for (int c = 0; c < width; c++) {

                for (int r = 0; r < height; r++) {
                    cur = Math.log10(this.matrices[i].get(r, c)) / loga;

                    if (cur < min) {
                        min = cur;
                    }

                    if (cur > max) {
                        max = cur;
                    }
                }
            }
        }

        LOG.log(Level.FINE, "Overall range: {0} to {1}", new Object[] { min, max });

        // Now, create a linear gradient of the logarithm values.
        for (int i = 0; i < this.range.length; i++) {
            cur = min + ((max - min) * i / this.range.length);
            this.range[i] = Math.pow(logBase, cur);
        }
    }

    /**
     * Draws the resulting images.
     */
    private void draw() {

        Dimension screen;
        int xCoord;
        int yCoord;
        int rowHeight;
        int maxWidth;
        JFrame frame;

        screen = Toolkit.getDefaultToolkit().getScreenSize();

        // Compute our size based on sizes of matrices, and compute the draw
        // location of each matrix.
        xCoord = 0;
        yCoord = 0;
        rowHeight = 0;
        maxWidth = 0;

        for (int i = 0; i < this.matrices.length; i++) {

            if ((xCoord + this.matrices[i].numColumns()) > screen.width) {
                xCoord = 0;
                yCoord += rowHeight;
                rowHeight = 0;
            }

            this.locations[i] = new Point(xCoord, yCoord); // NOPMD SRB
            xCoord += this.matrices[i].numColumns();

            if (xCoord > maxWidth) {
                maxWidth = xCoord;
            }

            if (this.matrices[i].numRows() > rowHeight) {
                rowHeight = this.matrices[i].numRows();
            }
        }

        yCoord += rowHeight;

        setPreferredSize(new Dimension(maxWidth, yCoord));
        setBackground(Color.BLACK);

        // Build a frame and set ourselves as the content panel
        frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setUndecorated(true);
        frame.setContentPane(this);
        frame.pack();
        frame.setVisible(true);
    }

    /**
     * Paints the panel.
     *
     * @param  grx  the <code>Graphics</code> to which to draw
     */
    @Override public void paintComponent(final Graphics grx) {

        Point loc;
        int width;
        int height;
        double cur;
        int color;

        super.paintComponent(grx);

        for (int i = 0; i < this.matrices.length; i++) {
            loc = this.locations[i];
            width = this.matrices[i].numColumns();
            height = this.matrices[i].numRows();

            for (int c = 0; c < width; c++) {

                for (int r = 0; r < height; r++) {
                    cur = this.matrices[i].get(r, c);
                    color = getColor(cur);
                    grx.setColor(this.gradient.getColor(color));
                    grx.drawLine(loc.x + c, loc.y + r, loc.x + c, loc.y + r);
                }
            }
        }
    }

    /**
     * Use the computed ranges to convert a value to the corresponding color.
     *
     * @param   value  the value
     * @return  the color index
     */
    private int getColor(final double value) {

        int color;

        color = this.range.length - 1;

        for (int i = 0; i < this.range.length; i++) {

            if (this.range[i] > value) {
                color = i;

                break;
            }
        }

        return color;
    }

    /**
     * Main method to run the program.
     *
     * @param  args  command-line arguments
     */
    public static void main(final String... args) {

        File[] files;
        ScaleUnifier unifier;

        files = new File[] {
                new File("/imp/radiusPitchLandscape-1.m3d"),
                new File("/imp/radiusPitchLandscape-2.m3d"),
                new File("/imp/radiusPitchLandscape-3.m3d")
            };

        try {
            unifier = new ScaleUnifier(4096, 1, files);
            unifier.makeScale(-1);
            unifier.draw();
        } catch (IOException e) {
            LOG.throwing("ScaleUnifier", "main", e);
        }
    }
}
