/*
 * Copyright 2019. Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.android.apps.santatracker.doodles.penguinswim;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import com.google.android.apps.santatracker.doodles.Config;
import com.google.android.apps.santatracker.doodles.shared.Vector2D;
import com.google.android.apps.santatracker.doodles.shared.actor.Actor;
import com.google.android.apps.santatracker.doodles.shared.physics.Polygon;
import com.google.android.apps.santatracker.doodles.shared.physics.Util;
import com.google.android.apps.santatracker.util.SantaLog;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Random;

/** One chunk of a level in the swimming game. */
public class SwimmingLevelChunk extends Actor {
    public static final int LEVEL_LENGTH_IN_METERS = 500;
    public static final int CHUNK_HEIGHT = 5000;
    public static final int NUM_ROWS = 100;
    public static final int NUM_COLS = 50;
    public static final float COL_WIDTH = SwimmingModel.LEVEL_WIDTH / (float) NUM_COLS;
    public static final float ROW_HEIGHT = CHUNK_HEIGHT / (float) NUM_ROWS;
    private static final String TAG = SwimmingLevelChunk.class.getSimpleName();
    private static final int SOLUTION_PATH_NUM_COLS = 50;
    private static final Random RANDOM = new Random();
    private static final List<String> TYPES;
    public static Queue<SwimmingLevelChunk> swimmingLevelChunks;
    public static List<SolutionPath> pathList;

    static {
        TYPES = new ArrayList<>();
        TYPES.add(BoundingBoxSpriteActor.ICE_CUBE);
        TYPES.add(BoundingBoxSpriteActor.DUCK);
        TYPES.add(BoundingBoxSpriteActor.HAND_GRAB);
    }

    public final float startY;
    public final float endY;
    private final float DEFAULT_OBSTACLE_DENSITY;
    public List<BoundingBoxSpriteActor> obstacles;
    private SolutionPath solutionPath;
    private boolean mirrored;

    private SwimmingLevelChunk(
            float startY, SolutionPath solutionPath, boolean mirrored, Context context) {

        // Get swimming obstacle density from config
        Config config = new Config();
        DEFAULT_OBSTACLE_DENSITY = (float) config.SWIMMING_OBSTACLE_DENSITY;

        this.solutionPath = solutionPath;
        this.mirrored = mirrored;
        this.startY = startY;
        generateObstacles(context);
        removeObstaclesFromSolutionPath(startY);
        this.endY = startY - solutionPath.getChunkHeight();
    }

    public static SwimmingLevelChunk create(float startY, Context context) {
        if (pathList == null || pathList.size() == 0) {
            loadChunkTemplates(context.getResources());
        }
        // Increase the probability that the random chunk will be a "middle open" chunk.
        int pathIndex = Math.min(pathList.size() - 1, RANDOM.nextInt(pathList.size() + 1));
        SolutionPath solutionPath = pathList.get(pathIndex);
        return new SwimmingLevelChunk(startY, solutionPath, RANDOM.nextBoolean(), context);
    }

    public static void generateAllLevelChunks(float startY, Context context) {
        long startTime = System.currentTimeMillis();
        swimmingLevelChunks = new LinkedList<>();
        SwimmingLevelChunk chunk = create(startY, context);
        while (SwimmingModel.getMetersFromWorldY(chunk.endY) < LEVEL_LENGTH_IN_METERS) {
            swimmingLevelChunks.add(chunk);
            chunk = create(chunk.endY, context);
        }
        SantaLog.d(
                TAG,
                "generateAllLevelChunks: finished in "
                        + ((System.currentTimeMillis() - startTime) / 1000.0f)
                        + " seconds.");
    }

    public static SwimmingLevelChunk getNextChunk() {
        if (!swimmingLevelChunks.isEmpty()) {
            return swimmingLevelChunks.remove();
        }
        return null;
    }

    public static void loadChunkTemplates(Resources res) {
        pathList = new ArrayList<>();

        Options decodeOptions = new Options();
        decodeOptions.inScaled = false;

        Bitmap b = BitmapFactory.decodeResource(res, R.raw.diamond, decodeOptions);
        pathList.add(new GridSolutionPath(b));

        b = BitmapFactory.decodeResource(res, R.raw.zig, decodeOptions);
        pathList.add(new GridSolutionPath(b));

        b = BitmapFactory.decodeResource(res, R.raw.ziggeroo, decodeOptions);
        pathList.add(new GridSolutionPath(b));

        b = BitmapFactory.decodeResource(res, R.raw.fork_in, decodeOptions);
        pathList.add(new GridSolutionPath(b));

        b = BitmapFactory.decodeResource(res, R.raw.fork_out, decodeOptions);
        pathList.add(new GridSolutionPath(b));

        b = BitmapFactory.decodeResource(res, R.raw.middle_open, decodeOptions);
        pathList.add(new GridSolutionPath(b));
    }

    @Override
    public void update(float deltaMs) {
        for (int i = 0; i < obstacles.size(); i++) {
            obstacles.get(i).update(deltaMs);
        }
    }

    @Override
    public void draw(Canvas canvas) {
        // solutionPath.draw(canvas, startY, mirrored);
        for (int i = 0; i < obstacles.size(); i++) {
            obstacles.get(i).draw(canvas);
        }
    }

    public void resolveCollisions(SwimmerActor swimmer, float deltaMs) {
        for (int i = 0; i < obstacles.size(); i++) {
            obstacles.get(i).resolveCollision(swimmer, deltaMs);
        }
    }

    private void generateObstacles(Context context) {
        obstacles = new ArrayList<>();
        for (int i = 0; i < solutionPath.getNumRows() * DEFAULT_OBSTACLE_DENSITY; i++) {
            float x = RANDOM.nextInt((4 * SwimmingModel.LEVEL_WIDTH) / 5);
            float y =
                    startY
                            - (SwimmingModel.LEVEL_WIDTH / 5)
                            - RANDOM.nextInt(
                                    (int) solutionPath.getChunkHeight()
                                            - (SwimmingModel.LEVEL_WIDTH / 5));

            int metersY = SwimmingModel.getMetersFromWorldY(y);
            int type;
            if (metersY < SwimmingModel.SCORE_THRESHOLDS[0]) {
                // Only show ice cubes before the bronze threshold.
                type = 0;
            } else if (metersY < SwimmingModel.SCORE_THRESHOLDS[1]) {
                // Show ice cubes and cans before the silver threshold.
                type = RANDOM.nextInt(TYPES.size() - 1);
            } else {
                // After the silver threshold, use all obstacles.
                boolean isInMiddleThreeLanes =
                        SwimmingModel.LEVEL_WIDTH / 5 <= x
                                && x <= 3 * SwimmingModel.LEVEL_WIDTH / 5;
                if (isInMiddleThreeLanes) {
                    // Only place octograbs in the middle three lanes. If we are generating an
                    // obstacle in the
                    // middle 3 lanes, give it a higher chance of being an octograb.
                    type = Math.min(TYPES.size() - 1, RANDOM.nextInt(TYPES.size() + 1));
                } else {
                    // If we are outside of the middle 3 lanes, give each other option equal weight.
                    type = RANDOM.nextInt(TYPES.size() - 1);
                }
            }

            BoundingBoxSpriteActor obstacle =
                    BoundingBoxSpriteActor.create(
                            Vector2D.get(x, y), TYPES.get(type), context.getResources());
            Polygon obstacleBody = obstacle.collisionBody;
            boolean shouldAdd = true;
            for (int j = 0; j < obstacles.size(); j++) {
                Polygon otherBody = obstacles.get(j).collisionBody;
                if (Util.rectIntersectsRect(
                        otherBody.min.x,
                        otherBody.min.y,
                        otherBody.getWidth(),
                        otherBody.getHeight(),
                        obstacleBody.min.x,
                        obstacleBody.min.y,
                        obstacleBody.getWidth(),
                        obstacleBody.getHeight())) {
                    shouldAdd = false;
                }
            }
            if (shouldAdd) {
                obstacles.add(obstacle);
            }
        }
    }

    private void removeObstaclesFromSolutionPath(float startY) {
        for (int i = obstacles.size() - 1; i >= 0; i--) {
            BoundingBoxSpriteActor obstacle = obstacles.get(i);
            Vector2D min = obstacle.collisionBody.min;
            Vector2D max = obstacle.collisionBody.max;
            if (solutionPath.intersects(startY, min.x, min.y, max.x, max.y, mirrored)) {
                obstacles.remove(i);
            }
        }
    }

    private interface SolutionPath {
        boolean intersects(
                float startY, float minX, float minY, float maxX, float maxY, boolean mirrored);

        void draw(Canvas canvas, float startY, boolean mirrored);

        int getEndCol(boolean mirrored);

        float getChunkHeight();

        int getNumRows();
    }

    private static class SolutionPathImpl implements SolutionPath {
        private static final int DRIFT_SAME = 0;
        private static final int DRIFT_REVERSE = 1;
        private final int[] driftDistribution = new int[] {50, 75, 100};
        private SolutionPathRow[] rows;
        private int endCol;
        private int drift;
        private Paint paint;

        public SolutionPathImpl(int startCol) {
            rows = new SolutionPathRow[NUM_ROWS];
            paint = new Paint();
            paint.setColor(Color.DKGRAY);

            for (int i = 0; i < rows.length; i++) {

                // Decide which way to drift.
                int driftToken = RANDOM.nextInt(100);
                if (driftToken < driftDistribution[DRIFT_SAME]) {
                    if (drift == 0) {
                        // If the path is going straight, switch it to a random direction.
                        drift = RANDOM.nextBoolean() ? 1 : -1;
                    }
                } else if (driftToken < driftDistribution[DRIFT_REVERSE]) {
                    drift = 0;
                } else {
                    drift *= -1;
                }
                if (startCol == 0) {
                    drift = 1;
                } else if (startCol == NUM_COLS - SOLUTION_PATH_NUM_COLS - 1) {
                    drift = -1;
                }
                startCol = Util.clamp(startCol + drift, 0, NUM_COLS - SOLUTION_PATH_NUM_COLS - 1);

                rows[i] = new SolutionPathRow(startCol, SOLUTION_PATH_NUM_COLS);
            }
            this.endCol = startCol;
        }

        @Override
        public boolean intersects(
                float startY, float minX, float minY, float maxX, float maxY, boolean mirrored) {
            // Subtract y from startY because the level proceeds in the negative y direction.
            int minRowIndex = (int) Math.max(0, (startY - maxY) / ROW_HEIGHT);
            int maxRowIndex = (int) Math.max(0, (startY - minY) / ROW_HEIGHT);
            for (int i = minRowIndex; i <= maxRowIndex; i++) {
                if (rows[i].intersects(Math.min(minX, SwimmingModel.LEVEL_WIDTH), mirrored)
                        || rows[i].intersects(
                                Math.min(maxX, SwimmingModel.LEVEL_WIDTH), mirrored)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public void draw(Canvas canvas, float startY, boolean mirrored) {
            float y = startY;
            for (int i = 0; i < rows.length; i++) {
                float startX = rows[i].startX;
                float endX = rows[i].endX;
                if (mirrored) {
                    startX = SwimmingModel.LEVEL_WIDTH - rows[i].endX;
                    endX = SwimmingModel.LEVEL_WIDTH - rows[i].startX;
                }
                canvas.drawRect(startX, y, endX, y + ROW_HEIGHT, paint);
                y -= ROW_HEIGHT;
            }
        }

        @Override
        public int getEndCol(boolean mirrored) {
            return mirrored ? NUM_COLS - 1 - endCol : endCol;
        }

        @Override
        public float getChunkHeight() {
            return CHUNK_HEIGHT;
        }

        @Override
        public int getNumRows() {
            return rows.length;
        }
    }

    private static class GridSolutionPath implements SolutionPath {

        public final float chunkHeight;
        public final int numRows;
        public final float rowHeight;

        private boolean[][] grid;
        private Paint paint;

        /**
         * Initialize this solution path with a bitmap. The length of the solution path will scale
         * with the height of the image, where 1px in the image = 1 grid unit in the chunk. The
         * width of the solution path is fixed to NUM_COLS and will just sample the bitmap at
         * NUM_COLS points. Any bitmap with a higher horizontal resolution than NUM_COLS will be
         * down-sampled, and any bitmap with a lower resolution will have single pixels being
         * sampled more than once horizontally.
         *
         * <p>In order to maintain visual consistency with the supplied bitmap, it is recommended
         * that the input PNGs are 50px wide.
         */
        public GridSolutionPath(Bitmap bitmap) {
            int bitmapWidth = bitmap.getWidth();
            int bitmapHeight = bitmap.getHeight();
            chunkHeight = bitmapHeight * COL_WIDTH;
            numRows = bitmapHeight;
            rowHeight = chunkHeight / numRows;

            SantaLog.d(TAG, "bitmapHeight: " + bitmapHeight);
            SantaLog.d(TAG, "chunkHeight: " + chunkHeight);
            SantaLog.d(TAG, "numRows: " + numRows);
            SantaLog.d(TAG, "rowHeight: " + rowHeight);

            paint = new Paint();
            paint.setColor(Color.DKGRAY);
            grid = new boolean[numRows][NUM_COLS];

            for (int i = 0; i < grid.length; i++) {
                for (int j = 0; j < grid[0].length; j++) {
                    int bitmapX = (int) ((((float) j) / grid[0].length) * bitmapWidth);
                    int bitmapY = (int) ((((float) i) / grid.length) * bitmapHeight);
                    int pixel = bitmap.getPixel(bitmapX, bitmapY);
                    grid[i][j] = (pixel & 0x00ffffff) != 0;
                }
            }
        }

        @Override
        public boolean intersects(
                float startY, float minX, float minY, float maxX, float maxY, boolean mirrored) {
            if (mirrored) {
                float tmpMinX = minX;
                minX = SwimmingModel.LEVEL_WIDTH - maxX;
                maxX = SwimmingModel.LEVEL_WIDTH - tmpMinX;
            }

            // Subtract y from startY because the level proceeds in the negative y direction.
            int minRowIndex = (int) Math.max(0, (startY - maxY) / rowHeight);
            int maxRowIndex = (int) Math.max(0, (startY - minY) / rowHeight);
            int minColIndex = (int) Math.min(NUM_COLS - 1, minX / COL_WIDTH);
            int maxColIndex = (int) Math.min(NUM_COLS - 1, maxX / COL_WIDTH);
            for (int i = minRowIndex; i <= maxRowIndex; i++) {
                for (int j = minColIndex; j <= maxColIndex; j++) {
                    if (grid[i][j]) {
                        return true;
                    }
                }
            }
            return false;
        }

        @Override
        public void draw(Canvas canvas, float startY, boolean mirrored) {
            float y = startY - rowHeight;
            for (int i = 0; i < grid.length; i++) {
                float x = 0;
                for (int j = 0; j < grid[0].length; j++) {
                    if (!mirrored && grid[i][j]) {
                        canvas.drawRect(x, y, x + COL_WIDTH, y + rowHeight, paint);
                    } else if (mirrored && grid[i][grid[0].length - 1 - j]) {
                        canvas.drawRect(x, y, x + COL_WIDTH, y + rowHeight, paint);
                    }
                    x += COL_WIDTH;
                }
                y -= rowHeight;
            }
        }

        @Override
        public int getEndCol(boolean mirrored) {
            return mirrored ? NUM_COLS - 1 : 0;
        }

        @Override
        public float getChunkHeight() {
            return chunkHeight;
        }

        @Override
        public int getNumRows() {
            return numRows;
        }
    }

    private static class SolutionPathRow {
        // The first column which is in the solution path.
        public final int startCol;
        // The column after the last column in the solution path.
        public final int endCol;
        public final float startX;
        public final float endX;

        public SolutionPathRow(int startCol, int numCols) {
            this.startCol = startCol;
            this.endCol = startCol + numCols;
            this.startX = startCol * COL_WIDTH;
            this.endX = endCol * COL_WIDTH;
        }

        public boolean intersects(float x, boolean mirrored) {
            float startX = this.startX;
            float endX = this.endX;
            if (mirrored) {
                startX = SwimmingModel.LEVEL_WIDTH - this.endX;
                endX = SwimmingModel.LEVEL_WIDTH - this.startX;
            }
            return startX <= x && x <= endX;
        }
    }
}
