'use strict';

var dot = require('./matrix').dot;
var BADNUM = require('../constants/numerical').BADNUM;

var polygon = module.exports = {};

/**
 * Turn an array of [x, y] pairs into a polygon object
 * that can test if points are inside it
 *
 * @param ptsIn Array of [x, y] pairs
 *
 * @returns polygon Object {xmin, xmax, ymin, ymax, pts, contains}
 *      (x|y)(min|max) are the bounding rect of the polygon
 *      pts is the original array, with the first pair repeated at the end
 *      contains is a function: (pt, omitFirstEdge)
 *          pt is the [x, y] pair to test
 *          omitFirstEdge truthy means points exactly on the first edge don't
 *              count. This is for use adding one polygon to another so we
 *              don't double-count the edge where they meet.
 *          returns boolean: is pt inside the polygon (including on its edges)
 */
polygon.tester = function tester(ptsIn) {
    var pts = ptsIn.slice();
    var xmin = pts[0][0];
    var xmax = xmin;
    var ymin = pts[0][1];
    var ymax = ymin;
    var i;

    if(
        pts[pts.length - 1][0] !== pts[0][0] ||
        pts[pts.length - 1][1] !== pts[0][1]
    ) {
        // close the polygon
        pts.push(pts[0]);
    }

    for(i = 1; i < pts.length; i++) {
        xmin = Math.min(xmin, pts[i][0]);
        xmax = Math.max(xmax, pts[i][0]);
        ymin = Math.min(ymin, pts[i][1]);
        ymax = Math.max(ymax, pts[i][1]);
    }

    // do we have a rectangle? Handle this here, so we can use the same
    // tester for the rectangular case without sacrificing speed

    var isRect = false;
    var rectFirstEdgeTest;

    if(pts.length === 5) {
        if(pts[0][0] === pts[1][0]) { // vert, horz, vert, horz
            if(pts[2][0] === pts[3][0] &&
                    pts[0][1] === pts[3][1] &&
                    pts[1][1] === pts[2][1]) {
                isRect = true;
                rectFirstEdgeTest = function(pt) { return pt[0] === pts[0][0]; };
            }
        } else if(pts[0][1] === pts[1][1]) { // horz, vert, horz, vert
            if(pts[2][1] === pts[3][1] &&
                    pts[0][0] === pts[3][0] &&
                    pts[1][0] === pts[2][0]) {
                isRect = true;
                rectFirstEdgeTest = function(pt) { return pt[1] === pts[0][1]; };
            }
        }
    }

    function rectContains(pt, omitFirstEdge) {
        var x = pt[0];
        var y = pt[1];

        if(x === BADNUM || x < xmin || x > xmax || y === BADNUM || y < ymin || y > ymax) {
            // pt is outside the bounding box of polygon
            return false;
        }
        if(omitFirstEdge && rectFirstEdgeTest(pt)) return false;

        return true;
    }

    function contains(pt, omitFirstEdge) {
        var x = pt[0];
        var y = pt[1];

        if(x === BADNUM || x < xmin || x > xmax || y === BADNUM || y < ymin || y > ymax) {
            // pt is outside the bounding box of polygon
            return false;
        }

        var imax = pts.length;
        var x1 = pts[0][0];
        var y1 = pts[0][1];
        var crossings = 0;
        var i;
        var x0;
        var y0;
        var xmini;
        var ycross;

        for(i = 1; i < imax; i++) {
            // find all crossings of a vertical line upward from pt with
            // polygon segments
            // crossings exactly at xmax don't count, unless the point is
            // exactly on the segment, then it counts as inside.
            x0 = x1;
            y0 = y1;
            x1 = pts[i][0];
            y1 = pts[i][1];
            xmini = Math.min(x0, x1);

            if(x < xmini || x > Math.max(x0, x1) || y > Math.max(y0, y1)) {
                // outside the bounding box of this segment, it's only a crossing
                // if it's below the box.

                continue;
            } else if(y < Math.min(y0, y1)) {
                // don't count the left-most point of the segment as a crossing
                // because we don't want to double-count adjacent crossings
                // UNLESS the polygon turns past vertical at exactly this x
                // Note that this is repeated below, but we can't factor it out
                // because
                if(x !== xmini) crossings++;
            } else {
                // inside the bounding box, check the actual line intercept

                // vertical segment - we know already that the point is exactly
                // on the segment, so mark the crossing as exactly at the point.
                if(x1 === x0) ycross = y;
                // any other angle
                else ycross = y0 + (x - x0) * (y1 - y0) / (x1 - x0);

                // exactly on the edge: counts as inside the polygon, unless it's the
                // first edge and we're omitting it.
                if(y === ycross) {
                    if(i === 1 && omitFirstEdge) return false;
                    return true;
                }

                if(y <= ycross && x !== xmini) crossings++;
            }
        }

        // if we've gotten this far, odd crossings means inside, even is outside
        return crossings % 2 === 1;
    }

    // detect if poly is degenerate
    var degenerate = true;
    var lastPt = pts[0];
    for(i = 1; i < pts.length; i++) {
        if(lastPt[0] !== pts[i][0] || lastPt[1] !== pts[i][1]) {
            degenerate = false;
            break;
        }
    }

    return {
        xmin: xmin,
        xmax: xmax,
        ymin: ymin,
        ymax: ymax,
        pts: pts,
        contains: isRect ? rectContains : contains,
        isRect: isRect,
        degenerate: degenerate
    };
};

/**
 * Test if a segment of a points array is bent or straight
 *
 * @param pts Array of [x, y] pairs
 * @param start the index of the proposed start of the straight section
 * @param end the index of the proposed end point
 * @param tolerance the max distance off the line connecting start and end
 *      before the line counts as bent
 * @returns boolean: true means this segment is bent, false means straight
 */
polygon.isSegmentBent = function isSegmentBent(pts, start, end, tolerance) {
    var startPt = pts[start];
    var segment = [pts[end][0] - startPt[0], pts[end][1] - startPt[1]];
    var segmentSquared = dot(segment, segment);
    var segmentLen = Math.sqrt(segmentSquared);
    var unitPerp = [-segment[1] / segmentLen, segment[0] / segmentLen];
    var i;
    var part;
    var partParallel;

    for(i = start + 1; i < end; i++) {
        part = [pts[i][0] - startPt[0], pts[i][1] - startPt[1]];
        partParallel = dot(part, segment);

        if(partParallel < 0 || partParallel > segmentSquared ||
            Math.abs(dot(part, unitPerp)) > tolerance) return true;
    }
    return false;
};

/**
 * Make a filtering polygon, to minimize the number of segments
 *
 * @param pts Array of [x, y] pairs (must start with at least 1 pair)
 * @param tolerance the maximum deviation from straight allowed for
 *      removing points to simplify the polygon
 *
 * @returns Object {addPt, raw, filtered}
 *      addPt is a function(pt: [x, y] pair) to add a raw point and
 *          continue filtering
 *      raw is all the input points
 *      filtered is the resulting filtered Array of [x, y] pairs
 */
polygon.filter = function filter(pts, tolerance) {
    var ptsFiltered = [pts[0]];
    var doneRawIndex = 0;
    var doneFilteredIndex = 0;

    function addPt(pt) {
        pts.push(pt);
        var prevFilterLen = ptsFiltered.length;
        var iLast = doneRawIndex;
        ptsFiltered.splice(doneFilteredIndex + 1);

        for(var i = iLast + 1; i < pts.length; i++) {
            if(i === pts.length - 1 || polygon.isSegmentBent(pts, iLast, i + 1, tolerance)) {
                ptsFiltered.push(pts[i]);
                if(ptsFiltered.length < prevFilterLen - 2) {
                    doneRawIndex = i;
                    doneFilteredIndex = ptsFiltered.length - 1;
                }
                iLast = i;
            }
        }
    }

    if(pts.length > 1) {
        var lastPt = pts.pop();
        addPt(lastPt);
    }

    return {
        addPt: addPt,
        raw: pts,
        filtered: ptsFiltered
    };
};
