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 ordered list of trajectories.
 */
public class TrajectoryListPipeItem extends AbstractPipeItem {

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

    /** the list of trajectories */
    private final transient List<Trajectory> trajectories;

    static {
        String crlf;

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

    /**
     * Constructs a new <code>TrajectoryListPipeItem</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 TrajectoryListPipeItem(final String theKey, final String theLabel, final Pipe thePipe) {

        super(theKey, theLabel, thePipe);

        PipeItemFileInfo info;

        this.trajectories = new ArrayList<Trajectory>(10);

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

    /**
     * Adds a trajectory to the end of the list.
     *
     * @param  traj  the trajectory to add
     */
    public void addTrajectory(final Trajectory traj) {

        this.trajectories.add(traj);
        getFile(0).notPersisted();
    }

    /**
     * Gets the number of trajectories in the series
     *
     * @return  the number of trajectories
     */
    public int getNumTrajectories() {

        return this.trajectories.size();
    }

    /**
     * Get a particular trajectory from the series.
     *
     * @param   index  the index of the trajectory to get
     * @return  the trajectory
     */
    public Trajectory getTrajectory(final int index) {

        return this.trajectories.get(index);
    }

    /**
     * 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 "Trajectory List";
    }

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

        this.trajectories.clear();
        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> on successful save; <code>false</code> on failure
     */
    @Override public boolean save(final FilterTreeExecutor executor, final int startPct,
        final int endPct) {

        PipeItemFileInfo info;
        StringBuilder str;
        ImagePoint point;
        boolean result;

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

        info = getFile(0);

        if (this.trajectories.isEmpty()) {
            info.notPersisted();
            result = false;
        } else {
            str = new StringBuilder(500);

            str.append("<trajectory-list>");
            str.append(CRLF);

            // Write out the ordered list of time points
            for (Trajectory traj : this.trajectories) {
                str.append("  <trajectory>");
                str.append(CRLF);

                for (int i = 0; i < traj.numPoints(); i++) {
                    str.append("    <point time='");
                    str.append(traj.getTimePoint(i));
                    str.append("' plane='");
                    str.append(traj.getPlane(i));

                    point = traj.getPoint(i);
                    str.append("' x='");
                    str.append(Integer.toString(point.getXPos()));
                    str.append("' y='");
                    str.append(Integer.toString(point.getYPos()));
                    str.append("' x-vel='");
                    str.append(Integer.toString(point.getXVel()));
                    str.append("' y-vel='");
                    str.append(Integer.toString(point.getYVel()));
                    str.append("' x-ambient-vel='");
                    str.append(Integer.toString(point.getXAmbientVel()));
                    str.append("' y-ambient-vel='");
                    str.append(Integer.toString(point.getYAmbientVel()));

                    str.append("'/>");
                    str.append(CRLF);
                }

                str.append("  </trajectory>");
                str.append(CRLF);
            }

            str.append("</trajectory-list>");
            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[] data;
        XmlParser parser;
        List<Node> parsed;
        NonemptyElement top;
        boolean result;

        info = getFile(0);

        reset();

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

        if (data == null) {
            info.notPersisted();
            result = false;
        } else {
            parser = new XmlParser();

            try {
                parsed = parser.parse(new String(data), true);

                // Should be one node at top level, "time-series"
                if ((parsed != null) && (parsed.size() == 1)
                        && (parsed.get(0) instanceof NonemptyElement)) {

                    top = (NonemptyElement) parsed.get(0);

                    if ("trajectory-list".equals(top.tagName)) {
                        result = processTopLevel(top);
                    } else {
                        LOG.warning("Missing <trajectory-list> element");
                        result = false;
                    }
                } else {
                    LOG.warning("Missing <trajectory-list> element");
                    result = false;
                }

            } catch (ParseException e) {
                LOG.log(Level.WARNING, "Exception while parsing XML", e);
                result = false;
            }

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

        return result;
    }

    /**
     * Processes the top-level "trajectory-list" node by verifying that all contained nodes are
     * either <trajectory> nodes, and parsing them as they are found.
     *
     * @param   top  the top-level node to parse
     * @return  <code>true</code> if the node is valid; <code>false</code> if not
     */
    private boolean processTopLevel(final NonemptyElement top) {

        NonemptyElement nonempty;
        boolean result;

        result = true;

        for (Node node : top.children) {

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

                if ("trajectory".equals(nonempty.tagName)) {
                    result = processTrajectory(nonempty);
                } else {
                    LOG.log(Level.WARNING, "Invalid element ''{0}'' within <trajectory-list>",
                        nonempty.tagName);
                    result = false;

                    break;
                }
            } else if (node instanceof EmptyElement) {
                LOG.log(Level.WARNING, "Invalid element ''{0}'' within <trajectory-list>",
                    ((EmptyElement) node).tagName);
                result = false;

                break;
            }
        }

        return result;
    }

    /**
     * Processes a "trajectory" node by verifying that all contained nodes are <point> nodes, and
     * parsing them as they are found.
     *
     * @param   elem  the trajectory node to parse
     * @return  <code>true</code> if the node is valid; <code>false</code> if not
     */
    private boolean processTrajectory(final NonemptyElement elem) {

        Trajectory traj;
        EmptyElement empty;
        ImagePoint point;
        boolean result;

        traj = new Trajectory();
        result = true;

        for (Node node : elem.children) {

            if (node instanceof NonemptyElement) {
                LOG.log(Level.WARNING, "Invalid element ''{0}'' within <trajectory>",
                    ((NonemptyElement) node).tagName);
                result = false;

                break;
            } else if (node instanceof EmptyElement) {
                empty = (EmptyElement) node;

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

                    try {
                        point = new ImagePoint(Integer.parseInt(empty.get("x")), // NOPMD SRB
                                Integer.parseInt(empty.get("y")),
                                Integer.parseInt(empty.get("x-vel")),
                                Integer.parseInt(empty.get("y-vel")),
                                Integer.parseInt(empty.get("x-ambient-vel")),
                                Integer.parseInt(empty.get("y-ambient-vel")));
                        traj.addPoint(Integer.parseInt(empty.get("time")),
                            Integer.parseInt(empty.get("plane")), point);
                    } catch (NumberFormatException e) {
                        LOG.warning("Invalid value in <point>");
                        result = false;

                        break;
                    } catch (NullPointerException e) {
                        LOG.warning("Missing value in <point>");
                        result = false;

                        break;
                    }
                } else {
                    LOG.log(Level.WARNING, "Invalid element ''{0}'' within <trajectory>",
                        empty.tagName);
                    result = false;

                    break;
                }
            }
        }

        if (result) {
            this.trajectories.add(traj);
        }

        return result;
    }

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

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