package squidpony.squidgrid;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import squidpony.squidmath.Coord;
import squidpony.squidmath.OrderedSet;

/**
 * Basic radius strategy implementations likely to be used for roguelikes.
 *
 * @author Eben Howard - http://squidpony.com - howard@squidpony.com
 */
public enum Radius {

    /**
     * In an unobstructed area the FOV would be a square.
     *
     * This is the shape that would represent movement radius in an 8-way
     * movement scheme with no additional cost for diagonal movement.
     */
    SQUARE,
    /**
     * In an unobstructed area the FOV would be a diamond.
     *
     * This is the shape that would represent movement radius in a 4-way
     * movement scheme.
     */
    DIAMOND,
    /**
     * In an unobstructed area the FOV would be a circle.
     *
     * This is the shape that would represent movement radius in an 8-way
     * movement scheme with all movement cost the same based on distance from
     * the source
     */
    CIRCLE,
    /**
     * In an unobstructed area the FOV would be a cube.
     *
     * This is the shape that would represent movement radius in an 8-way
     * movement scheme with no additional cost for diagonal movement.
     */
    CUBE,
    /**
     * In an unobstructed area the FOV would be a octahedron.
     *
     * This is the shape that would represent movement radius in a 4-way
     * movement scheme.
     */
    OCTAHEDRON,
    /**
     * In an unobstructed area the FOV would be a sphere.
     *
     * This is the shape that would represent movement radius in an 8-way
     * movement scheme with all movement cost the same based on distance from
     * the source
     */
    SPHERE;

    private static final double PI2 = Math.PI * 2;
    public double radius(int startx, int starty, int startz, int endx, int endy, int endz) {
        return radius((double) startx, (double) starty, (double) startz, (double) endx, (double) endy, (double) endz);
    }

    public double radius(double startx, double starty, double startz, double endx, double endy, double endz) {
        double dx = Math.abs(startx - endx);
        double dy = Math.abs(starty - endy);
        double dz = Math.abs(startz - endz);
        return radius(dx, dy, dz);
    }

    public double radius(int dx, int dy, int dz) {
        return radius((float) dx, (float) dy, (float) dz);
    }

    public double radius(double dx, double dy, double dz) {
        dx = Math.abs(dx);
        dy = Math.abs(dy);
        dz = Math.abs(dz);
        double radius = 0;
        switch (this) {
            case SQUARE:
            case CUBE:
                radius = Math.max(dx, Math.max(dy, dz));//radius is longest axial distance
                break;
            case DIAMOND:
            case OCTAHEDRON:
                radius = dx + dy + dz;//radius is the manhattan distance
                break;
            case CIRCLE:
            case SPHERE:
                radius = Math.sqrt(dx * dx + dy * dy + dz * dz);//standard spherical radius
        }
        return radius;
    }

    public double radius(int startx, int starty, int endx, int endy) {
        return radius((double) startx, (double) starty, (double) endx, (double) endy);
    }
    public double radius(Coord start, Coord end) {
        return radius((double) start.x, (double) start.y, (double) end.x, (double) end.y);
    }
    public double radius(Coord end) {
        return radius(0.0, 0.0, (double) end.x, (double) end.y);
    }

    public double radius(double startx, double starty, double endx, double endy) {
        double dx = startx - endx;
        double dy = starty - endy;
        return radius(dx, dy);
    }

    public double radius(int dx, int dy) {
        return radius((double) dx, (double) dy);
    }

    public double radius(double dx, double dy) {
        return radius(dx, dy, 0);
    }

    public double volume2D(double radiusLength)
    {
        switch (this) {
            case SQUARE:
            case CUBE:
                return (radiusLength * 2 + 1) * (radiusLength * 2 + 1);
            case DIAMOND:
            case OCTAHEDRON:
                return radiusLength * (radiusLength + 1) * 2 + 1;
            default:
                return Math.PI * radiusLength * radiusLength + 1;
        }
    }
    public double volume3D(double radiusLength)
    {
        switch (this) {
            case SQUARE:
            case CUBE:
                return (radiusLength * 2 + 1) * (radiusLength * 2 + 1) * (radiusLength * 2 + 1);
            case DIAMOND:
            case OCTAHEDRON:
                double total = radiusLength * (radiusLength + 1) * 2 + 1;
                for(double i = radiusLength - 1; i >= 0; i--)
                {
                    total += (i * (i + 1) * 2 + 1) * 2;
                }
                return total;
            default:
                return Math.PI * radiusLength * radiusLength * radiusLength * 4.0 / 3.0 + 1;
        }
    }




    private int clamp(int n, int min, int max)
    {
        return Math.min(Math.max(min, n), max - 1);
    }

    public Set<Coord> perimeter(Coord center, int radiusLength, boolean surpassEdges, int width, int height)
    {
        OrderedSet<Coord> rim = new OrderedSet<>(4 * radiusLength);
        if(!surpassEdges && (center.x < 0 || center.x >= width || center.y < 0 || center.y > height))
            return rim;
        if(radiusLength < 1) {
            rim.add(center);
            return rim;
        }
        switch (this) {
            case SQUARE:
            case CUBE:
            {
                for (int i = center.x - radiusLength; i <= center.x + radiusLength; i++) {
                    int x = i;
                    if(!surpassEdges) x = clamp(i, 0, width);
                    rim.add(Coord.get(x, clamp(center.y - radiusLength, 0, height)));
                    rim.add(Coord.get(x, clamp(center.y + radiusLength, 0, height)));
                }
                for (int j = center.y - radiusLength; j <= center.y + radiusLength; j++) {
                    int y = j;
                    if(!surpassEdges) y = clamp(j, 0, height);
                    rim.add(Coord.get(clamp(center.x - radiusLength, 0, height), y));
                    rim.add(Coord.get(clamp(center.x + radiusLength, 0, height), y));
                }
            }
            break;
            case DIAMOND:
            case OCTAHEDRON: {
                int xUp = center.x + radiusLength, xDown = center.x - radiusLength,
                        yUp = center.y + radiusLength, yDown = center.y - radiusLength;
                if(!surpassEdges) {
                    xDown = clamp(xDown, 0, width);
                    xUp = clamp(xUp, 0, width);
                    yDown = clamp(yDown, 0, height);
                    yUp = clamp(yUp, 0, height);
                }

                rim.add(Coord.get(xDown, center.y));
                rim.add(Coord.get(xUp, center.y));
                rim.add(Coord.get(center.x, yDown));
                rim.add(Coord.get(center.x, yUp));

                for (int i = xDown + 1, c = 1; i < center.x; i++, c++) {
                    int x = i;
                    if(!surpassEdges) x = clamp(i, 0, width);
                    rim.add(Coord.get(x, clamp(center.y - c, 0, height)));
                    rim.add(Coord.get(x, clamp(center.y + c, 0, height)));
                }
                for (int i = center.x + 1, c = 1; i < center.x + radiusLength; i++, c++) {
                    int x = i;
                    if(!surpassEdges) x = clamp(i, 0, width);
                    rim.add(Coord.get(x, clamp(center.y + radiusLength - c, 0, height)));
                    rim.add(Coord.get(x, clamp(center.y - radiusLength + c, 0, height)));
                }
            }
            break;
            default:
            {
                double theta;
                int x, y, denom = 1;
                boolean anySuccesses;
                while(denom <= 256) {
                    anySuccesses = false;
                    for (int i = 1; i <= denom; i+=2)
                    {
                        theta = i * (PI2 / denom);
                        x = (int) Math.round(Math.cos(theta) * radiusLength) + center.x;
                        y = (int) Math.round(Math.sin(theta) * radiusLength) + center.y;
                        if (!surpassEdges) {
                            x = clamp(x, 0, width);
                            y = clamp(y, 0, height);
                        }
                        Coord p = Coord.get(x, y);
                        boolean test = rim.contains(p);

                        rim.add(p);
                        anySuccesses = test || anySuccesses;
                    }
                    if(!anySuccesses)
                        break;
                    denom *= 2;
                }

            }
        }
        return rim;
    }
    public Coord extend(Coord center, Coord middle, int radiusLength, boolean surpassEdges, int width, int height)
    {
        if(!surpassEdges && (center.x < 0 || center.x >= width || center.y < 0 || center.y > height ||
                middle.x < 0 || middle.x >= width || middle.y < 0 || middle.y > height))
            return Coord.get(0, 0);
        if(radiusLength < 1) {
            return center;
        }
        double theta = Math.atan2(middle.y - center.y, middle.x - center.x),
                cosTheta = Math.cos(theta), sinTheta = Math.sin(theta);

        Coord end = Coord.get(middle.x, middle.y);
        switch (this) {
            case SQUARE:
            case CUBE:
            case DIAMOND:
            case OCTAHEDRON:
            {
                int rad2 = 0;
                if(surpassEdges)
                {
                    while (radius(center.x, center.y, end.x, end.y) < radiusLength) {
                        rad2++;
                        end = Coord.get((int) Math.round(cosTheta * rad2) + center.x
                                , (int) Math.round(sinTheta * rad2) + center.y);
                    }
                }
                else {
                    while (radius(center.x, center.y, end.x, end.y) < radiusLength) {
                        rad2++;
                        end = Coord.get(clamp((int) Math.round(cosTheta * rad2) + center.x, 0, width)
                                      , clamp((int) Math.round(sinTheta * rad2) + center.y, 0, height));
                        if (end.x == 0 || end.x == width - 1 || end.y == 0 || end.y == height - 1)
                            return end;
                    }
                }

                return end;
            }
            default:
            {
                end = Coord.get(clamp( (int) Math.round(cosTheta * radiusLength) + center.x, 0, width)
                        , clamp( (int) Math.round(sinTheta * radiusLength) + center.y, 0, height));
                if(!surpassEdges) {
                    long edgeLength = 0;
//                    if (end.x == 0 || end.x == width - 1 || end.y == 0 || end.y == height - 1)
                    if (end.x < 0)
                    {
                        // wow, we lucked out here. the only situation where cos(angle) is 0 is if the angle aims
                        // straight up or down, and then x cannot be < 0 or >= width.
                        edgeLength = Math.round((0 - center.x) / cosTheta);
                        end = end.setY(clamp((int) Math.round(sinTheta * edgeLength) + center.y, 0, height));
                    }
                    else if(end.x >= width)
                    {
                        // wow, we lucked out here. the only situation where cos(angle) is 0 is if the angle aims
                        // straight up or down, and then x cannot be < 0 or >= width.
                        edgeLength = Math.round((width - 1 - center.x) / cosTheta);
                        end = end.setY(clamp((int) Math.round(sinTheta * edgeLength) + center.y, 0, height));
                    }

                    if (end.y < 0)
                    {
                        // wow, we lucked out here. the only situation where sin(angle) is 0 is if the angle aims
                        // straight left or right, and then y cannot be < 0 or >= height.
                        edgeLength = Math.round((0 - center.y) / sinTheta);
                        end = end.setX(clamp((int) Math.round(cosTheta * edgeLength) + center.x, 0, width));
                    }
                    else if(end.y >= height)
                    {
                        // wow, we lucked out here. the only situation where sin(angle) is 0 is if the angle aims
                        // straight left or right, and then y cannot be < 0 or >= height.
                        edgeLength = Math.round((height - 1 - center.y) / sinTheta);
                        end = end.setX(clamp((int) Math.round(cosTheta * edgeLength) + center.x, 0, width));
                    }
                }
                return end;
            }
        }
    }

    /**
     * Compares two Radius enums as if they are both in a 2D plane; that is, Radius.SPHERE is treated as equal to
     * Radius.CIRCLE, Radius.CUBE is equal to Radius.SQUARE, and Radius.OCTAHEDRON is equal to Radius.DIAMOND.
     * @param other the Radius to compare this to
     * @return true if the 2D versions of both Radius enums are the same shape.
     */
    public boolean equals2D(Radius other)
    {
        switch (this)
        {
            case CIRCLE:
            case SPHERE:
                return other == CIRCLE || other == SPHERE;
            case SQUARE:
            case CUBE:
                return other == SQUARE || other == CUBE;
            default:
                return other == DIAMOND || other == OCTAHEDRON;
        }
    }
    public boolean inRange(int startx, int starty, int endx, int endy, int minRange, int maxRange)
    {
        double dist = radius(startx, starty, endx, endy);
        return dist >= minRange - 0.001 && dist <= maxRange + 0.001;
    }

    public int roughDistance(int xPos, int yPos) {
        int x = Math.abs(xPos), y = Math.abs(yPos);
        switch (this) {
            case CIRCLE:
            case SPHERE:
            {
                if(x == y)
                    return 3 * x;
                else if(x < y)
                    return 3 * x + 2 * (y - x);
                else
                    return 3 * y + 2 * (x - y);
            }
            case DIAMOND:
            case OCTAHEDRON:
                return 2 * (x + y);
            default:
                return 2 * Math.max(x, y);
        }
    }

    public List<Coord> pointsInside(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height)
    {
        return pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, null);
    }
    public List<Coord> pointsInside(Coord center, int radiusLength, boolean surpassEdges, int width, int height)
    {
        if(center == null) return null;
        return pointsInside(center.x, center.y, radiusLength, surpassEdges, width, height, null);
    }

    public List<Coord> pointsInside(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height, List<Coord> buf)
    {
        List<Coord> contents = buf == null ? new ArrayList<Coord>((int)Math.ceil(volume2D(radiusLength))) : buf;
        if(!surpassEdges && (centerX < 0 || centerX >= width || centerY < 0 || centerY >= height))
            return contents;
        if(radiusLength < 1) {
            contents.add(Coord.get(centerX, centerY));
            return contents;
        }
        switch (this) {
            case SQUARE:
            case CUBE:
            {
                for (int i = centerX - radiusLength; i <= centerX + radiusLength; i++) {
                    for (int j = centerY - radiusLength; j <= centerY + radiusLength; j++) {
                        if(!surpassEdges && (i < 0 || j < 0 || i >= width || j >= height))
                            continue;
                        contents.add(Coord.get(i, j));
                    }
                }
            }
            break;
            case DIAMOND:
            case OCTAHEDRON: {
                for (int i = centerX - radiusLength; i <= centerX + radiusLength; i++) {
                    for (int j = centerY - radiusLength; j <= centerY + radiusLength; j++) {
                        if ((Math.abs(centerX - i) + Math.abs(centerY- j) > radiusLength) ||
                                (!surpassEdges && (i < 0 || j < 0 || i >= width || j >= height)))
                            continue;
                        contents.add(Coord.get(i, j));
                    }
                }
            }
            break;
            default:
            {
                int high;
                for (int dx = -radiusLength; dx <= radiusLength; ++dx) {
                    high = (int) Math.sqrt(radiusLength * radiusLength - dx * dx);
                    for (int dy = -high; dy <= high; ++dy) {
                        if (!surpassEdges && (centerX + dx < 0 || centerY + dy < 0 ||
                                centerX + dx >= width || centerY + dy >= height))
                            continue;
                        contents.add(Coord.get(centerX + dx, centerY + dy));
                    }
                }
            }
        }
        return contents;
    }

    /**
     * Gets a List of all Coord points within {@code radiusLength} of {@code center} using Chebyshev measurement (making
     * a square). Appends Coords to {@code buf} if it is non-null, and returns either buf or a freshly-allocated List of
     * Coord. If {@code surpassEdges} is false, which is the normal usage, this will not produce Coords with x or y less
     * than 0 or greater than {@code width} or {@code height}; if surpassEdges is true, then it can produce any Coords
     * in the actual radius.
     * @param centerX the center Coord x
     * @param centerY the center Coord x
     * @param radiusLength the inclusive distance from (centerX,centerY) for Coords to use in the List
     * @param surpassEdges usually should be false; if true, can produce Coords with negative x/y or past width/height
     * @param width the width of the area this can place Coords (exclusive, not relative to center, usually map width)
     * @param height the height of the area this can place Coords (exclusive, not relative to center, usually map height)
     * @return a new List containing the points within radiusLength of the center

     */
    public static List<Coord> inSquare(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height)
    {
        return SQUARE.pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, null);
    }
    /**
     * Gets a List of all Coord points within {@code radiusLength} of {@code center} using Manhattan measurement (making
     * a diamond). Appends Coords to {@code buf} if it is non-null, and returns either buf or a freshly-allocated List
     * of Coord. If {@code surpassEdges} is false, which is the normal usage, this will not produce Coords with x or y
     * less than 0 or greater than {@code width} or {@code height}; if surpassEdges is true, then it can produce any
     * Coords in the actual radius.
     * @param centerX the center Coord x
     * @param centerY the center Coord x
     * @param radiusLength the inclusive distance from (centerX,centerY) for Coords to use in the List
     * @param surpassEdges usually should be false; if true, can produce Coords with negative x/y or past width/height
     * @param width the width of the area this can place Coords (exclusive, not relative to center, usually map width)
     * @param height the height of the area this can place Coords (exclusive, not relative to center, usually map height)
     * @return a new List containing the points within radiusLength of the center
     */
    public static List<Coord> inDiamond(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height)
    {
        return DIAMOND.pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, null);
    }
    /**
     * Gets a List of all Coord points within {@code radiusLength} of {@code center} using Euclidean measurement (making
     * a circle). Appends Coords to {@code buf} if it is non-null, and returns either buf or a freshly-allocated List of
     * Coord. If {@code surpassEdges} is false, which is the normal usage, this will not produce Coords with x or y less
     * than 0 or greater than {@code width} or {@code height}; if surpassEdges is true, then it can produce any Coords
     * in the actual radius.
     * @param centerX the center Coord x
     * @param centerY the center Coord x
     * @param radiusLength the inclusive distance from (centerX,centerY) for Coords to use in the List
     * @param surpassEdges usually should be false; if true, can produce Coords with negative x/y or past width/height
     * @param width the width of the area this can place Coords (exclusive, not relative to center, usually map width)
     * @param height the height of the area this can place Coords (exclusive, not relative to center, usually map height)
     * @return a new List containing the points within radiusLength of the center
     */
    public static List<Coord> inCircle(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height)
    {
        return CIRCLE.pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, null);
    }

    /**
     * Gets a List of all Coord points within {@code radiusLength} of {@code center} using Chebyshev measurement (making
     * a square). Appends Coords to {@code buf} if it is non-null, and returns either buf or a freshly-allocated List of
     * Coord. If {@code surpassEdges} is false, which is the normal usage, this will not produce Coords with x or y less
     * than 0 or greater than {@code width} or {@code height}; if surpassEdges is true, then it can produce any Coords
     * in the actual radius.
     * @param centerX the center Coord x
     * @param centerY the center Coord x
     * @param radiusLength the inclusive distance from (centerX,centerY) for Coords to use in the List
     * @param surpassEdges usually should be false; if true, can produce Coords with negative x/y or past width/height
     * @param width the width of the area this can place Coords (exclusive, not relative to center, usually map width)
     * @param height the height of the area this can place Coords (exclusive, not relative to center, usually map height)
     * @param buf the List of Coord to append points to; may be null to create a new List
     * @return buf, after appending Coords to it, or a new List if buf was null
     */
    public static List<Coord> inSquare(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height, List<Coord> buf)
    {
        return SQUARE.pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, buf);
    }
    /**
     * Gets a List of all Coord points within {@code radiusLength} of {@code center} using Manhattan measurement (making
     * a diamond). Appends Coords to {@code buf} if it is non-null, and returns either buf or a freshly-allocated List
     * of Coord. If {@code surpassEdges} is false, which is the normal usage, this will not produce Coords with x or y
     * less than 0 or greater than {@code width} or {@code height}; if surpassEdges is true, then it can produce any
     * Coords in the actual radius.
     * @param centerX the center Coord x
     * @param centerY the center Coord x
     * @param radiusLength the inclusive distance from (centerX,centerY) for Coords to use in the List
     * @param surpassEdges usually should be false; if true, can produce Coords with negative x/y or past width/height
     * @param width the width of the area this can place Coords (exclusive, not relative to center, usually map width)
     * @param height the height of the area this can place Coords (exclusive, not relative to center, usually map height)
     * @param buf the List of Coord to append points to; may be null to create a new List
     * @return buf, after appending Coords to it, or a new List if buf was null
     */
    public static List<Coord> inDiamond(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height, List<Coord> buf)
    {
        return DIAMOND.pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, buf);
    }
    /**
     * Gets a List of all Coord points within {@code radiusLength} of {@code center} using Euclidean measurement (making
     * a circle). Appends Coords to {@code buf} if it is non-null, and returns either buf or a freshly-allocated List of
     * Coord. If {@code surpassEdges} is false, which is the normal usage, this will not produce Coords with x or y less
     * than 0 or greater than {@code width} or {@code height}; if surpassEdges is true, then it can produce any Coords
     * in the actual radius.
     * @param centerX the center Coord x
     * @param centerY the center Coord x
     * @param radiusLength the inclusive distance from (centerX,centerY) for Coords to use in the List
     * @param surpassEdges usually should be false; if true, can produce Coords with negative x/y or past width/height
     * @param width the width of the area this can place Coords (exclusive, not relative to center, usually map width)
     * @param height the height of the area this can place Coords (exclusive, not relative to center, usually map height)
     * @param buf the List of Coord to append points to; may be null to create a new List
     * @return buf, after appending Coords to it, or a new List if buf was null
     */
    public static List<Coord> inCircle(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height, List<Coord> buf)
    {
        return CIRCLE.pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, buf);
    }

    /**
     * Given an Iterable of Coord (such as a List or Set), a distance to expand outward by (using this Radius), and the
     * bounding height and width of the map, gets a "thickened" group of Coord as a Set where each Coord in points has
     * been expanded out by an amount no greater than distance. As an example, you could call this on a line generated
     * by Bresenham, OrthoLine, or an LOS object's getLastPath() method, and expand the line into a thick "brush stroke"
     * where this Radius affects the shape of the ends. This will never produce a Coord with negative x or y, a Coord
     * with x greater than or equal to width, or a Coord with y greater than or equal to height.
     * @param distance the distance, as measured by this Radius, to expand each Coord on points up to
     * @param width the bounding width of the map (exclusive)
     * @param height the bounding height of the map (exclusive)
     * @param points an Iterable (such as a List or Set) of Coord that this will make a "thickened" version of
     * @return a Set of Coord that covers a wider area than what points covers; each Coord will be unique (it's a Set)
     */
    public Set<Coord> expand(int distance, int width, int height, Iterable<Coord> points)
    {
        List<Coord> around = pointsInside(Coord.get(distance, distance), distance, false, width, height);
        OrderedSet<Coord> expanded = new OrderedSet<>(around.size() * 16);
        int tx, ty;
        for(Coord pt : points)
        {
            for(Coord ar : around)
            {
                tx = pt.x + ar.x - distance;
                ty = pt.y + ar.y - distance;
                if(tx >= 0 && tx < width && ty >= 0 && ty < height)
                    expanded.add(Coord.get(tx, ty));
            }
        }
        return expanded;
    }
}
