/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * 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
 *
 *      http://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.android.launcher3.celllayout;

import android.graphics.Point;
import android.graphics.Rect;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.stream.Collectors;


public class CellLayoutBoard implements Comparable<CellLayoutBoard> {

    private boolean intersects(Rect r1, Rect r2) {
        // If one rectangle is on left side of other
        if (r1.left > r2.right || r2.left > r1.right) {
            return false;
        }

        // If one rectangle is above other
        if (r1.bottom > r2.top || r2.bottom > r1.top) {
            return false;
        }

        return true;
    }

    private boolean overlapsWithIgnored(Set<Rect> ignoredRectangles, Rect rect) {
        for (Rect ignoredRect : ignoredRectangles) {
            // Using the built in intersects doesn't work because it doesn't account for area 0
            if (intersects(ignoredRect, rect)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int compareTo(CellLayoutBoard cellLayoutBoard) {
        // to be equal they need to have the same number of widgets and the same dimensions
        // their order can be different
        Set<Rect> widgetsSet = new HashSet<>();
        Set<Rect> ignoredRectangles = new HashSet<>();
        for (WidgetRect rect : mWidgetsRects) {
            if (rect.shouldIgnore()) {
                ignoredRectangles.add(rect.mBounds);
            } else {
                widgetsSet.add(rect.mBounds);
            }
        }
        for (WidgetRect rect : cellLayoutBoard.mWidgetsRects) {
            // ignore rectangles overlapping with the area marked by x
            if (overlapsWithIgnored(ignoredRectangles, rect.mBounds)) {
                continue;
            }
            if (!widgetsSet.contains(rect.mBounds)) {
                return -1;
            }
            widgetsSet.remove(rect.mBounds);
        }
        if (!widgetsSet.isEmpty()) {
            return 1;
        }

        // to be equal they need to have the same number of icons their order can be different
        Set<Point> iconsSet = new HashSet<>();
        mIconPoints.forEach(icon -> iconsSet.add(icon.getCoord()));
        for (IconPoint icon : cellLayoutBoard.mIconPoints) {
            if (!iconsSet.contains(icon.getCoord())) {
                return -1;
            }
            iconsSet.remove(icon.getCoord());
        }
        if (!iconsSet.isEmpty()) {
            return 1;
        }
        return 0;
    }

    public static class CellType {
        // The cells marked by this will be filled by 1x1 widgets and will be ignored when
        // validating
        public static final char IGNORE = 'x';
        // The cells marked by this will be filled by app icons
        public static final char ICON = 'i';
        // Empty space
        public static final char EMPTY = '-';
        // Widget that will be saved as "main widget" for easier retrieval
        public static final char MAIN_WIDGET = 'm';
        // Everything else will be consider a widget
    }

    public static class WidgetRect {
        public char mType;
        public Rect mBounds;

        WidgetRect(char type, Rect bounds) {
            this.mType = type;
            this.mBounds = bounds;
        }

        int getSpanX() {
            return mBounds.right - mBounds.left + 1;
        }

        int getSpanY() {
            return mBounds.top - mBounds.bottom + 1;
        }

        int getCellX() {
            return mBounds.left;
        }

        int getCellY() {
            return mBounds.bottom;
        }

        boolean shouldIgnore() {
            return this.mType == CellType.IGNORE;
        }

        @Override
        public String toString() {
            return "WidgetRect type = " + mType + " bounds = " + mBounds.toString();
        }
    }

    public static class IconPoint {
        public Point coord;
        public char mType;

        public IconPoint(Point coord, char type) {
            this.coord = coord;
            mType = type;
        }

        public char getType() {
            return mType;
        }

        public void setType(char type) {
            mType = type;
        }

        public Point getCoord() {
            return coord;
        }

        public void setCoord(Point coord) {
            this.coord = coord;
        }
    }

    static final int INFINITE = 99999;

    char[][] mWidget = new char[30][30];

    List<WidgetRect> mWidgetsRects = new ArrayList<>();
    Map<Character, WidgetRect> mWidgetsMap = new HashMap<>();

    List<IconPoint> mIconPoints = new ArrayList<>();
    Map<Character, IconPoint> mIconsMap = new HashMap<>();

    WidgetRect mMain = null;

    CellLayoutBoard() {
        for (int x = 0; x < mWidget.length; x++) {
            for (int y = 0; y < mWidget[0].length; y++) {
                mWidget[x][y] = CellType.EMPTY;
            }
        }
    }

    public List<WidgetRect> getWidgets() {
        return mWidgetsRects;
    }

    public List<IconPoint> getIcons() {
        return mIconPoints;
    }

    public WidgetRect getMain() {
        return mMain;
    }

    public WidgetRect getWidgetRect(char c) {
        return mWidgetsMap.get(c);
    }

    private void removeWidgetFromBoard(WidgetRect widget) {
        for (int xi = widget.mBounds.left; xi < widget.mBounds.right; xi++) {
            for (int yi = widget.mBounds.top; yi < widget.mBounds.bottom; yi++) {
                mWidget[xi][yi] = '-';
            }
        }
    }

    private void removeOverlappingItems(Rect rect) {
        // Remove overlapping widgets and remove them from the board
        mWidgetsRects = mWidgetsRects.stream().filter(widget -> {
            if (rect.intersect(widget.mBounds)) {
                removeWidgetFromBoard(widget);
                return false;
            }
            return true;
        }).collect(Collectors.toList());
        // Remove overlapping icons and remove them from the board
        mIconPoints = mIconPoints.stream().filter(iconPoint -> {
            int x = iconPoint.coord.x;
            int y = iconPoint.coord.y;
            if (rect.contains(x, y)) {
                mWidget[x][y] = '-';
                return false;
            }
            return true;
        }).collect(Collectors.toList());
    }

    private void removeOverlappingItems(Point p) {
        // Remove overlapping widgets and remove them from the board
        mWidgetsRects = mWidgetsRects.stream().filter(widget -> {
            if (widget.mBounds.contains(p.x, p.y)) {
                removeWidgetFromBoard(widget);
                return false;
            }
            return true;
        }).collect(Collectors.toList());
        // Remove overlapping icons and remove them from the board
        mIconPoints = mIconPoints.stream().filter(iconPoint -> {
            int x = iconPoint.coord.x;
            int y = iconPoint.coord.y;
            if (p.x == x && p.y == y) {
                mWidget[x][y] = '-';
                return false;
            }
            return true;
        }).collect(Collectors.toList());
    }

    public void addWidget(int x, int y, int spanX, int spanY, char type) {
        Rect rect = new Rect(x, y + spanY - 1, x + spanX - 1, y);
        removeOverlappingItems(rect);
        WidgetRect widgetRect = new WidgetRect(type, rect);
        mWidgetsRects.add(widgetRect);
        for (int xi = rect.left; xi < rect.right + 1; xi++) {
            for (int yi = rect.bottom; yi < rect.top + 1; yi++) {
                mWidget[xi][yi] = type;
            }
        }
    }

    public void addIcon(int x, int y) {
        removeOverlappingItems(new Point(x, y));
        mWidget[x][y] = 'i';
    }

    public static WidgetRect getWidgetRect(int x, int y, Set<Point> used, char[][] board) {
        char type = board[x][y];
        Queue<Point> search = new ArrayDeque<Point>();
        Point current = new Point(x, y);
        search.add(current);
        used.add(current);
        List<Point> neighbors = new ArrayList<>(List.of(
                new Point(-1, 0),
                new Point(0, -1),
                new Point(1, 0),
                new Point(0, 1))
        );
        Rect widgetRect = new Rect(INFINITE, -INFINITE, -INFINITE, INFINITE);
        while (!search.isEmpty()) {
            current = search.poll();
            widgetRect.top = Math.max(widgetRect.top, current.y);
            widgetRect.right = Math.max(widgetRect.right, current.x);
            widgetRect.bottom = Math.min(widgetRect.bottom, current.y);
            widgetRect.left = Math.min(widgetRect.left, current.x);
            for (Point p : neighbors) {
                Point next = new Point(current.x + p.x, current.y + p.y);
                if (next.x < 0 || next.x >= board.length) continue;
                if (next.y < 0 || next.y >= board[0].length) continue;
                if (board[next.x][next.y] == type && !used.contains(next)) {
                    used.add(next);
                    search.add(next);
                }
            }
        }
        return new WidgetRect(type, widgetRect);
    }

    public static boolean isWidget(char type) {
        return type != CellType.ICON && type != CellType.EMPTY;
    }

    public static boolean isIcon(char type) {
        return type == CellType.ICON;
    }

    private static List<WidgetRect> getRects(char[][] board) {
        Set<Point> used = new HashSet<>();
        List<WidgetRect> widgetsRects = new ArrayList<>();
        for (int x = 0; x < board.length; x++) {
            for (int y = 0; y < board[0].length; y++) {
                if (!used.contains(new Point(x, y)) && isWidget(board[x][y])) {
                    widgetsRects.add(getWidgetRect(x, y, used, board));
                }
            }
        }
        return widgetsRects;
    }

    private static List<IconPoint> getIconPoints(char[][] board) {
        List<IconPoint> iconPoints = new ArrayList<>();
        for (int x = 0; x < board.length; x++) {
            for (int y = 0; y < board[0].length; y++) {
                if (isIcon(board[x][y])) {
                    iconPoints.add(new IconPoint(new Point(x, y), board[x][y]));
                }
            }
        }
        return iconPoints;
    }

    public static WidgetRect getMainFromList(List<CellLayoutBoard> boards) {
        for (CellLayoutBoard board : boards) {
            WidgetRect main = board.getMain();
            if (main != null) {
                return main;
            }
        }
        return null;
    }

    public static CellLayoutBoard boardFromString(String boardStr) {
        String[] lines = boardStr.split("\n");
        CellLayoutBoard board = new CellLayoutBoard();

        for (int y = 0; y < lines.length; y++) {
            String line = lines[y];
            for (int x = 0; x < line.length(); x++) {
                char c = line.charAt(x);
                if (c != CellType.EMPTY) {
                    board.mWidget[x][y] = line.charAt(x);
                }
            }
        }
        board.mWidgetsRects = getRects(board.mWidget);
        board.mWidgetsRects.forEach(widgetRect -> {
            if (widgetRect.mType == CellType.MAIN_WIDGET) {
                board.mMain = widgetRect;
            }
            board.mWidgetsMap.put(widgetRect.mType, widgetRect);
        });
        board.mIconPoints = getIconPoints(board.mWidget);
        return board;
    }

    public String toString(int maxX, int maxY) {
        StringBuilder s = new StringBuilder();
        maxX = Math.min(maxX, mWidget.length);
        maxY = Math.min(maxY, mWidget[0].length);
        for (int y = 0; y < maxY; y++) {
            for (int x = 0; x < maxX; x++) {
                s.append(mWidget[x][y]);
            }
            s.append('\n');
        }
        return s.toString();
    }

    public static List<CellLayoutBoard> boardListFromString(String boardsStr) {
        String[] lines = boardsStr.split("\n");
        ArrayList<String> individualBoards = new ArrayList<>();
        ArrayList<CellLayoutBoard> boards = new ArrayList<>();
        for (String line : lines) {
            String[] boardSegment = line.split("\\|");
            for (int i = 0; i < boardSegment.length; i++) {
                if (i >= individualBoards.size()) {
                    individualBoards.add(boardSegment[i]);
                } else {
                    individualBoards.set(i, individualBoards.get(i) + "\n" + boardSegment[i]);
                }
            }
        }
        for (String board : individualBoards) {
            boards.add(CellLayoutBoard.boardFromString(board));
        }
        return boards;
    }
}
