package edu.oregonstate.cartography.simplefeatures;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;

public class LineString extends Geometry {

    //Stores Point objects in an ArrayList called linePoints
    ArrayList<Point> points = new ArrayList<>();

    //Returns the number of points
    public int getNumPoints() {
        return points.size();
    }

    //Returns point at specified index
    public Point getPointN(int index) {
        return points.get(index);
    }
    
    public Point getFirstPoint() {
        return points.isEmpty() ? null : points.get(0);
    }
    
    public Point getLastPoint() {
        return points.isEmpty() ? null : points.get(points.size() - 1);
    }

    //Points of line field
    public ArrayList<Point> getPoints() {
        return points;
    }

    /**
     * Returns the length of this line.
     *
     * @return
     */
    public double getLength() {
        int nPoints = getNumPoints();
        if (nPoints < 2) {
            return 0;
        }
        double length = 0;
        Point p1 = getPointN(0);
        for (int i = 1; i < nPoints; i++) {
            Point p2 = getPointN(i);
            double dx = p1.getX() - p2.getX();
            double dy = p1.getY() - p2.getY();
            length += Math.sqrt(dx * dx + dy * dy);
            p1 = p2;
        }
        return length;
    }

    //Tests if first and last point are identical and returns true if they are
    //If true, line is closed
    public boolean isClosed() {
        Point point1 = points.get(0);
        Point point2 = points.get(points.size() - 1);
        return (point1.getX() == point2.getX() && point1.getY() == point2.getY());
    }

    //Appends a passed point to list of existing points
    public void addPoint(Point addedPoint) {
        points.add(addedPoint);
    }

    public void addPointIfDifferentFromLast(Point p) {
        int nbrPoints = getNumPoints();
        if (nbrPoints == 0) {
            this.addPoint(p);
        } else {
            Point lastPoint = getPointN(nbrPoints - 1);
            if (!lastPoint.isSameLocation(p)) {
                points.add(p);
            }
        }
    }
    
    //Calls the toString method of each point and concatenates returned strings 
    //to a single string and returns the concatenated string
    @Override
    public String toString() {
        String lineDesc = /*"Attributes:\n" + super.toString() + */"Line:" + "\n";
        for (Point point : points) {
            String desc = point.toString();
            lineDesc += desc + "\n";
        }
        return lineDesc;
    }

    @Override
    public void paint(Graphics2D g2d) {
        //Sets color of lines
        g2d.setColor(Color.green);
        //Creates a GeneralPath shape
        GeneralPath.Double path = new GeneralPath.Double();

        //Retrieves the first point
        Point firstPoint = getPointN(0);
        //Pass x and y coordinates of the first point in path.moveTo()
        path.moveTo(firstPoint.getX(), firstPoint.getY());

        //Starts at index 1 and iterates over all remaining points, then 
        //calls path.lineTo
        for (int i = 1; i < points.size(); i++) {
            Point nextPoint = getPointN(i);
            path.lineTo(nextPoint.getX(), nextPoint.getY());

        }

        //Paints line
        g2d.draw(path);
    }

    @Override
    public Rectangle2D getBoundingBox() {

        //Returns null if linePoints ArrayList does not have any points
        if (points.size() < 1) {
            return null;
        }

        //Retrieves bounding box of first point of line
        Point firstPoint = getPointN(0);
        //Assign bounding box to variable
        Rectangle2D bb = firstPoint.getBoundingBox();
        //Iterate over remaining points and retrieve bounding boxes
        for (Point point : points) {
            //creates new bounding box that includes previous points and current point
            bb = bb.createUnion(point.getBoundingBox());
        }
        return bb;
    }

    public LineString densify(double maxSegmentLength) {
        LineString denseLine = new LineString();
        if (getNumPoints() == 0) {
            return denseLine;
        }

        // copy first point
        Point p1 = getPointN(0);
        denseLine.addPoint(p1);

        // densify line segment
        for (int i = 1; i < getNumPoints(); i++) {
            Point p2 = getPointN(i);
            double x1 = p1.getX();
            double y1 = p1.getY();
            double x2 = p2.getX();
            double y2 = p2.getY();
            double segmentLength = Math.hypot(x1 - x2, y1 - y2);
            if (segmentLength > maxSegmentLength) {
                double dx = (x2 - x1) / segmentLength;
                double dy = (y2 - y1) / segmentLength;
                int nbrIntermediatePoints = (int) (segmentLength / maxSegmentLength);
                for (int j = 0; j < nbrIntermediatePoints; j++) {
                    double x = x1 + j * dx * maxSegmentLength;
                    double y = y1 + j * dy * maxSegmentLength;
                    denseLine.addPoint(new Point(x, y));
                }
            }
            // add last point
            denseLine.addPoint(p2);
            p1 = p2;
        }
        return denseLine;
    }

    public void smoothPointAttribute(int filterSize, String sourceAttributeName, 
            String targetAttributeName) {
        
        if (filterSize % 2 != 1 || filterSize <= 0) {
            throw new IllegalArgumentException("filter size must be positive odd number");
        }
        
        if (getNumPoints() == 0 || filterSize == 1) {
            return;
        }
        
        int halfFilterSize = filterSize / 2;
        int nbrPoints = getNumPoints();
        for (int i = 0; i < nbrPoints; i++) {
            double total = 0;
            int nbrPointsInFilter = 0;
            for (int j = -halfFilterSize; j <= halfFilterSize; j++) {
                int ptID = i + j;
                if (ptID >= 0 && ptID < nbrPoints) {
                    Point pt = getPointN(ptID);
                    total += pt.getAttribute(sourceAttributeName).doubleValue();
                    nbrPointsInFilter++;
                }
            }
            double smoothValue = total / nbrPointsInFilter;
            getPointN(i).setAttribute(targetAttributeName, smoothValue);
        }
    }
    /*
    public static void main (String[]args) {
        LineString line = new LineString();
        for (int i = 0; i < 10; i++) {
            Point p = new Point(i, i);
            p.setAttribute("in", i*i);
            line.addPoint(p);
        }
        line.smoothPointAttribute(5, "in", "out");
        System.out.println(line);
    }
    */
    
    /**
     * Invert order of points.
     */
    public void invert() {
        int nPoints = getNumPoints();
        for (int i = 0; i < nPoints / 2; i++) {
            Point p1 = getPointN(i);
            Point p2 = getPointN(nPoints - i - 1);
            points.set(nPoints - i - 1, p1);
            points.set(i, p2);
        }
    }

    /**
     * Append line
     *
     * @param l2
     */
    public void append(LineString l2) {
        if (l2 == null || l2.getNumPoints() == 0) {
            return;
        }
        int nPoints = l2.getNumPoints();
        for (int i = 0; i < nPoints; i++) {
            Point p = l2.getPointN(i);
            addPointIfDifferentFromLast(p);
        }
    }
    
    /**
     * Joins two lines. Inverts order of points in lines if necessary. Does not
     * modify passed lines.
     *
     * @param l1
     * @param l2
     * @param centralPt
     * @return
     */
    public static LineString concatenateLines(LineString l1, LineString l2, Point centralPt) {
        if (l1.getFirstPoint().isSameLocation(centralPt)) {
            l1.invert();
        }
        if (!l2.getFirstPoint().isSameLocation(centralPt)) {
            l2.invert();
        }
        LineString combinedLine = new LineString();
        combinedLine.append(l1);
        combinedLine.append(l2);
        return combinedLine;
    }
}
