package com.srbenoit.filter.items;

import java.io.File;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import com.srbenoit.filter.AbstractPipeItem;
import com.srbenoit.filter.FilterTreeExecutor;
import com.srbenoit.filter.Pipe;
import com.srbenoit.filter.PipeItemFileInfo;
import com.srbenoit.xml.EmptyElement;
import com.srbenoit.xml.Node;
import com.srbenoit.xml.NonemptyElement;
import com.srbenoit.xml.XmlParser;

/**
 * An array of point sets. The intent is that an image array has a list of points associated with
 * each image, so we would have a point set array of the same dimensions as our image array.
 */
public class PointSetArrayPipeItem extends AbstractPipeItem {

    /** end of line characters */
    private static final String CRLF;

    /** the point sets - need list of list of lists to maintain type safety. */
    private transient List<List<List<ImagePoint>>> pointSets;

    static {
        String crlf;

        crlf = System.getProperty("line.separator");
        CRLF = (crlf == null) ? "\n" : crlf;
    }

    /**
     * Constructs a new empty <code>PointSetArrayPipeItem</code>.
     *
     * @param  theKey    the unique key for the item
     * @param  theLabel  the label for the item (a human friendly name)
     * @param  thePipe   the pipe in which this item is installed
     */
    public PointSetArrayPipeItem(final String theKey, final String theLabel, final Pipe thePipe) {

        super(theKey, theLabel, thePipe);

        PipeItemFileInfo info;

        buildPointLists(0, 0);

        info = new PipeItemFileInfo(makeFile(getSubdir()));
        addFile(info);
    }

    /**
     * Constructs a new <code>PointSetArrayPipeItem</code> of a given size and type.
     *
     * @param  theKey    the unique key for the item
     * @param  theLabel  the label for the item (a human friendly name)
     * @param  thePipe   the pipe in which this item is installed
     * @param  numX      the number of images in the X (or time) direction
     * @param  numY      the number if images in the Y (or plane) direction
     */
    public PointSetArrayPipeItem(final String theKey, final String theLabel, final Pipe thePipe,
        final int numX, final int numY) {

        super(theKey, theLabel, thePipe);

        PipeItemFileInfo info;

        if ((numX < 1) || (numY < 1)) {
            throw new IllegalArgumentException("Each array dimension must be at least 1");
        }

        buildPointLists(numX, numY);

        info = new PipeItemFileInfo(makeFile(getSubdir()));
        addFile(info);
    }

    /**
     * Gets the size of the image array along the X direction.
     *
     * @return  the array X dimension
     */
    public int getXSize() {

        return this.pointSets.size();
    }

    /**
     * Gets the size of the image array along the Y direction.
     *
     * @return  the array Y dimension
     */
    public int getYSize() {

        return this.pointSets.get(0).size();
    }

    /**
     * Gets a human-friendly name for the data type. For example, a list of sets of images
     * representing a time series of z-planes might return "Multi-plane image sequence".
     *
     * @return  the name of the data type this item represents
     */
    @Override public String typeName() {

        return "Point Set Array";
    }

    /**
     * Resets the pipe item to a virgin (empty) state.
     */
    @Override public void reset() {

        for (List<List<ImagePoint>> list : this.pointSets) {

            for (List<ImagePoint> inner : list) {
                inner.clear();
            }
        }

        getFile(0).notPersisted();
    }

    /**
     * Gets the number of points in the point set at a particular X and Y position.
     *
     * @param   xPos  the X position
     * @param   yPos  the Y position
     * @return  the number of points in the point set
     */
    public int getNumPoints(final int xPos, final int yPos) {

        return this.pointSets.get(xPos).get(yPos).size();
    }

    /**
     * Gets a point from the point set at a particular X and Y position.
     *
     * @param   xPos   the X position
     * @param   yPos   the Y position
     * @param   index  the point index to retrieve
     * @return  the point
     */
    public ImagePoint getPoint(final int xPos, final int yPos, final int index) {

        return this.pointSets.get(xPos).get(yPos).get(index);
    }

    /**
     * Clears the point set at a particular X and Y position.
     *
     * @param  xPos  the X position
     * @param  yPos  the Y position
     */
    public void clear(final int xPos, final int yPos) {

        this.pointSets.get(xPos).get(yPos).clear();
        getFile(0).notPersisted();
    }

    /**
     * Adds a point to the point set at a particular X and Y position.
     *
     * @param  xPos   the X position
     * @param  yPos   the Y position
     * @param  point  the point to add
     */
    public void addPoint(final int xPos, final int yPos, final ImagePoint point) {

        this.pointSets.get(xPos).get(yPos).add(point);
        getFile(0).notPersisted();
    }

    /**
     * Sets the point at a particular index in a point set at a particular X and Y position.
     *
     * @param  xPos   the X position
     * @param  yPos   the Y position
     * @param  index  the index of the point to set
     * @param  point  the point to add
     */
    public void setPoint(final int xPos, final int yPos, final int index, final ImagePoint point) {

        this.pointSets.get(xPos).get(yPos).set(index, point);
        getFile(0).notPersisted();
    }

    /**
     * Saves the item to a filesystem.
     *
     * @param   executor  the executor that is saving the pipe
     * @param   startPct  the starting progress percentage for the save operation
     * @param   endPct    the ending progress percentage for the save operation
     * @return  <code>true</code> if the save succeeded; <code>false</code> if not
     */
    @Override public boolean save(final FilterTreeExecutor executor, final int startPct,
        final int endPct) {

        PipeItemFileInfo info;
        StringBuilder str;
        boolean result;
        List<ImagePoint> points;

        executor.indicateProgress((startPct + endPct) / 2);

        info = getFile(0);

        str = new StringBuilder(500);
        str.append("<point-set-array x-len='");
        str.append(Integer.toString(getXSize()));
        str.append("' y-len='");
        str.append(Integer.toString(getYSize()));
        str.append("'>");
        str.append(CRLF);

        for (int x = 0; x < getXSize(); x++) {

            for (int y = 0; y < getYSize(); y++) {
                str.append(" <point-set x='");
                str.append(x);
                str.append("' y='");
                str.append(y);
                str.append("'>");
                str.append(CRLF);

                points = this.pointSets.get(x).get(y);

                for (ImagePoint point : points) {
                    str.append(" <point x='");
                    str.append(point.getXPos());
                    str.append("' y='");
                    str.append(point.getYPos());
                    str.append("' vx='");
                    str.append(point.getXVel());
                    str.append("' vy='");
                    str.append(point.getYVel());
                    str.append("' vxAmbient='");
                    str.append(point.getXAmbientVel());
                    str.append("' vyAmbient='");
                    str.append(point.getYAmbientVel());
                    str.append("'/>");
                    str.append(CRLF);
                }

                str.append("</point-set>");
                str.append(CRLF);
            }
        }

        str.append("</point-set-array>");
        str.append(CRLF);

        if (Pipe.writeFile(info.getFile(), str.toString().getBytes())) {
            info.wasPersisted();
            result = true;
        } else {
            info.notPersisted();
            result = false;
        }

        return result;
    }

    /**
     * Loads the items from the filesystem.
     *
     * @return  <code>true</code> if the load succeeded; <code>false</code> if not
     */
    @Override public boolean load() {

        PipeItemFileInfo info;
        byte[] bytes;
        List<Node> nodes;
        NonemptyElement nonempty;
        String xLen;
        String yLen;
        int numX;
        int numY;
        boolean result;

        info = getFile(0);

        reset();

        bytes = Pipe.readFile(info.getFile());

        if (bytes == null) {
            result = false;
            info.notPersisted();
        } else {

            try {
                nodes = new XmlParser().parse(new String(bytes), true);

                if ((nodes.size() == 1) && (nodes.get(0) instanceof NonemptyElement)) {
                    nonempty = (NonemptyElement) nodes.get(0);
                    xLen = nonempty.get("x-len");
                    yLen = nonempty.get("y-len");

                    try {
                        numX = Integer.parseInt(xLen);
                        numY = Integer.parseInt(yLen);

                        if (nonempty.children.size() == (numX * numY)) {
                            buildPointLists(numX, numY);
                            result = populatePointLists(nonempty.children);
                        } else {
                            LOG.warning(
                                "Invalid number of <point-set> entries while loading PointSetArray");
                            result = false;
                        }
                    } catch (NumberFormatException e) {
                        LOG.warning("Invalid length while loading PointSetArray");
                        result = false;
                    }
                } else {
                    LOG.warning("Unable to parse PointSetArray xml file");
                    result = false;
                }
            } catch (ParseException e) {
                LOG.warning("Unable to parse PointSetArray xml file");
                result = false;
            }

            if (result) {
                info.wasPersisted();
            } else {
                info.notPersisted();
            }
        }

        return result;
    }

    /**
     * Builds the list of lists of points that stores the data.
     *
     * @param  numX  the number of point sets along the X axis
     * @param  numY  the number of point sets along the Y axis
     */
    private void buildPointLists(final int numX, final int numY) {

        List<List<ImagePoint>> list;

        // First list is indexed by 'x'
        this.pointSets = new ArrayList<List<List<ImagePoint>>>(numX);

        for (int i = 0; i < numX; i++) {

            // Second list is indexed by 'y'
            list = new ArrayList<List<ImagePoint>>(numY); // NOPMD SRB
            this.pointSets.add(list);

            for (int j = 0; j < numY; j++) {

                // Create the interior point lists (empty)
                list.add(new ArrayList<ImagePoint>(20)); // NOPMD SRB
            }
        }
    }

    /**
     * Populates the points lists from a parsed XML node that should contain only "point-set"
     * elements.
     *
     * @param   nodes  the list of "point-set" nodes
     * @return  <code>true</code> if successful loading, <code>false</code> otherwise
     */
    private boolean populatePointLists(final List<Node> nodes) {

        NonemptyElement nonempty;
        String xStr;
        String yStr;
        int xPos;
        int yPos;
        List<ImagePoint> list;
        boolean result = true;

        for (Node node : nodes) {

            if (node instanceof NonemptyElement) {
                nonempty = (NonemptyElement) node;

                if ("point-set".equals(nonempty.tagName)) {
                    xStr = nonempty.get("x");
                    yStr = nonempty.get("y");

                    try {
                        xPos = Integer.parseInt(xStr);
                        yPos = Integer.parseInt(yStr);

                        list = this.pointSets.get(xPos).get(yPos);
                        result = populatePoints(list, nonempty);

                        if (!result) {
                            break;
                        }

                    } catch (NumberFormatException e) {
                        LOG.warning(
                            "Invalid x/y coordinate in <point-set> while loading PointSetArray");
                        result = false;

                        break;
                    }
                } else {
                    LOG.log(Level.WARNING, "Invalid element ''{0}'' while loading PointSetArray",
                        nonempty.tagName);
                    result = false;

                    break;
                }
            } else {
                LOG.warning("Invalid element while loading PointSetArray");
                result = false;

                break;
            }
        }

        return result;
    }

    /**
     * Populates a single point set from "point" elements in a parsed XML node.
     *
     * @param   points    the list to which to add the extracted points
     * @param   nonemtpy  the list containing the "point" elements
     * @return  <code>true</code> if successful loading, <code>false</code> otherwise
     */
    private boolean populatePoints(final List<ImagePoint> points, final NonemptyElement nonempty) {

        EmptyElement empty;
        String xStr;
        String yStr;
        String vxStr;
        String vyStr;
        String vxAmbStr;
        String vyAmbStr;
        int xPos;
        int yPos;
        int xVel;
        int yVel;
        int xVelAmb;
        int yVelAmb;
        boolean result = true;

        for (Node node : nonempty.children) {

            if (node instanceof EmptyElement) {
                empty = (EmptyElement) node;

                if ("point".equals(empty.tagName)) {

                    xStr = empty.get("x");
                    yStr = empty.get("y");
                    vxStr = empty.get("vx");
                    vyStr = empty.get("vy");
                    vxAmbStr = empty.get("vxAmbient");
                    vyAmbStr = empty.get("vyAmbient");

                    try {
                        xPos = Integer.parseInt(xStr);
                        yPos = Integer.parseInt(yStr);
                        xVel = Integer.parseInt(vxStr);
                        yVel = Integer.parseInt(vyStr);
                        xVelAmb = Integer.parseInt(vxAmbStr);
                        yVelAmb = Integer.parseInt(vyAmbStr);
                        points.add(new ImagePoint(xPos, yPos, xVel, yVel, xVelAmb, yVelAmb)); // NOPMD SRB
                    } catch (NumberFormatException e) {
                        LOG.warning(
                            "Invalid x/y coordinate in <point> while loading PointSetArray");
                        result = false;

                        break;
                    }
                } else {
                    LOG.log(Level.WARNING, "Invalid element ''{0}'' while loading PointSetArray",
                        empty.tagName);
                    result = false;

                    break;
                }
            } else {
                LOG.warning("Invalid element while loading PointSetArray");
                result = false;

                break;
            }
        }

        return result;
    }

    /**
     * Creates a file that represents the object data.
     *
     * @param   dir  the directory where the pipe's items are stored
     * @return  the file
     */
    private File makeFile(final File dir) {

        return new File(dir, getKey() + "_PointSetArray.xml");
    }
}
