package org.artisanlogiciel.games.maze.gui;

import org.artisanlogiciel.games.maze.*;
import org.artisanlogiciel.games.maze.solve.DirectionPosition;
import org.artisanlogiciel.games.maze.solve.MazeResolutionListener;
import org.artisanlogiciel.graphics.Drawing;
import org.artisanlogiciel.graphics.DrawingLine;
import org.artisanlogiciel.xpm.Xpm;

import javax.swing.*;
import java.awt.*;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;

public class MazeComponent
        extends JComponent
        implements MazeCreationListener,
        MazeResolutionListener,
        MouseMotionListener,
        Scrollable
{
    private static final long serialVersionUID = 3163272907991176390L;

    // WallsProvider map;
    LabyModel map;
    final MazeCellParameters cp;
    Position current = null;
    LinkedList<DirectionPosition> solvedPath = null;
    LinkedList<DirectionPosition> drawingPath = null;
    final Object lockChange = new Object();
    // current postion of human resolving
    int sX = 0;
    int sY = 0;
    // goal exit
    int gX = -1;
    int gY = -1;

    // use a background
    Xpm xpm = null;

    Date lastDrag = null;
    // delay after which a draging to draw a continuous line is ended, a new line will then start.
    long dragTimeout = MazeDefault.dragTimeout;

    // not set by default for debug, will show any path
    boolean showAll = false;

    StatusListener statusListener;

    MazeColorMap colorMap = new MazeColorMap(Color.white, Color.black, Color.blue, Color.green, Color.red);

    public class MazeColorMap
    {
        public Color background;
        public Color wall;
        public Color path;
        public Color resolved_path;
        public Color goal;

        public MazeColorMap(Color background, Color wall, Color path, Color resolved_path, Color goal) {
            this.background = background;
            this.wall = wall;
            this.path = path;
            this.resolved_path = resolved_path;
            this.goal = goal;
        }
    }

    public void setXpm(Xpm xpm)
    {
        this.xpm = xpm;
    }

    // for a given (x,y) pixel return cell position.

    Position getPosition(int x, int y) {
        // if rightmost position of this view  not (0,0) this is not working.
        // that's why MousListener should be attache to this MazeComponent and not to Scrollable one...
        return cp.toMazeCoordinates(x,y);
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        boolean add = ((e.getModifiersEx() & InputEvent.BUTTON1_DOWN_MASK) != 0);
        // where pixel -> maze coordinate is done
        Position newPosition = getPosition(e.getX(), e.getY());
        Date now = new Date(System.currentTimeMillis());
        if (lastDrag == null) {
            resetDrawingPath();
        } else {
            if (now.getTime() - lastDrag.getTime() > dragTimeout) {
                resetDrawingPath();
                statusListener.addStatus("move timeout");
            }
        }
        lastDrag = now;
        addPosition(newPosition, add);
        changed(null, null, map);
    }

    public void resetDrawingPath() {
        drawingPath = null;
    }

    public void addDrawing(Drawing drawing, boolean add) {
        for (DrawingLine line : drawing.getInternLines()) {
            addDrawingLine(line, add);
        }
        changed(null, null, map);
    }

    public void addDrawingLine(DrawingLine line, boolean add) {
        resetDrawingPath();
        ArrayList<Point> points = line.getPoints();
        for (Point p : points) {
            addPosition(new Position(p.x, p.y), add);
        }
    }

    // button 1 : add direction; button 2 : replace with direction.
    // Position is Already in maze coordinates (ie not pixels)
    public void addPosition(Position newPosition, boolean add) {
        // should find the cell ...
        DirectionPosition last = null;
        short path = 0;
        if (drawingPath == null) {
            drawingPath = new LinkedList<>();
            last = new DirectionPosition((short) 0, newPosition);
            statusListener.addStatus("Mouse dragged Cell " + newPosition + " Button  " + add);
            drawingPath.addLast(last);
        } else {
            // setShowAll(true);
            DirectionPosition first = drawingPath.getLast();
            last = first;
            // construct as many move form last to new position as needed.
            while (!last.getPosition().equals(newPosition)) {
                path = LabyModel.getDirection(last.getPosition(), newPosition);
                last.setDirection(path);
                if (add) {
                    map.addDirection(last.getPosition().getX(), last.getPosition().getY(), path);
                } else {
                    map.setDirection(last.getPosition().getX(), last.getPosition().getY(), path);
                }
                last = last.moveToAdjacentDirection();
                drawingPath.addLast(last);
            }
            statusListener.addStatus("Mouse dragged from Cell " + first.getPosition() + "To" + newPosition + " Button  " + add);
        }
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        // addStatus("Mouse moved (" + e.getX() + ',' + e.getY() + ')');
        Position newPosition = getPosition(e.getX(), e.getY());
        if ((newPosition.getX() >= 0) && (newPosition.getY() >= 0)) {
            requestFocus();
        }
    }

    public void setShowAll(boolean showall) {
        this.showAll = showall;
        statusListener.addStatus(showAll ? "show all" : "X");
    }

    void checkExit() {
        if ((sX == gX) && (sY == gY)) {
            statusListener.addStatus("Exit found by human !");
        }
    }

    private void proceed() {
        // should redraw ...
        invalidate();
        repaint();
        checkExit();
    }

    void goNorth() {
        resetResolution();
        if (map.canMoveInDirection(sX, sY, Brick.UP)) {
            sY = sY - 1;
            proceed();
        }
    }

    void goSouth() {
        resetResolution();
        if (map.canMoveInDirection(sX, sY, Brick.DOWN)) {
            sY = sY + 1;
            proceed();
        }
    }

    void goEast() {
        resetResolution();
        if (map.canMoveInDirection(sX, sY, Brick.RIGHT)) {
            sX = sX + 1;
            proceed();
        }
    }

    void goWest() {
        resetResolution();
        if (map.canMoveInDirection(sX, sY, Brick.LEFT)) {
            sX = sX - 1;
            proceed();
        }
    }

    /**
     * width, height of one cell,
     * offsetX, offsetY of upper left corner
     **/
    // public MazeComponent(WallsProvider map, MazeCellParameters cp)
    public MazeComponent(LabyModel map, MazeCellParameters cp, StatusListener statusListener) {
        super();
        this.cp = cp;
        this.map = map;
        setPreferredSize(cp.getDimension());
        gX = map.getWidth() - 1;
        gY = map.getHeight() - 1;
        this.statusListener = statusListener;
        addMouseMotionListener(this);
    }

    // public void resetWallsProvider(WallsProvider map)
    public void resetWallsProvider(LabyModel map) {
        this.map = map;
        solvedPath = null;
        // could be kept
        drawingPath = null;
        sX = 0;
        sY = 0;
        cp.resetMazeWidthHeight(map.getWidth(), map.getHeight());
        setPreferredSize(cp.getDimension());
    }

    public void setWallSize(int size) {
        cp.setCellSize((double) size, (double) size);
        // should redraw ...
        invalidate();
        repaint();
    }

    public int getAutoSize() {
        Rectangle r = getBounds();
        // cp.resetMazeWidthHeight( (int)r.getWidth(), (int) r.getHeight());
        cp.adaptTo(r.getWidth(), r.getHeight());
        // should redraw ...
        invalidate();
        repaint();
        return 50;
    }

    public void changed(Position cl, Position cr, WallsProvider map) {
        // should redraw ...
        invalidate();
        repaint();
        /* what was this ?

        Object waiter = new Object();
        synchronized (waiter) {
            try {
                waiter.wait(10);
            } catch (InterruptedException e) {
                System.err.println("Interrupted !");
            }
        }
        */
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        int x = 0;
        int y = 0;
        short walls = 0;
        short path = 0;

        // try to display only visible part...
        // should compute pX, pY, mX, mY based on clip...
        // Shape s=g.getClip();
        Rectangle r = g.getClipBounds();

        int mX = (int) ((double) r.getWidth() / cp.getWidth());
        int mY = (int) ((double) r.getHeight() / cp.getHeight());
        int pX = (int) ((double) (r.getX() - cp.getOffsetX()) / cp.getWidth());
        int pY = (int) ((double) (r.getY() - cp.getOffsetY()) / cp.getHeight());

        if (pX < 0)
            pX = 0;
        if (pY < 0)
            pY = 0;
        if (pX >= map.getWidth())
            return;
        if (pY >= map.getHeight())
            return;

        mX = mX + pX;
        mY = mY + pY;

        if (mX > map.getWidth()) {
            mX = map.getWidth();
        }
        if (mY > map.getHeight()) {
            mY = map.getHeight();
        }

        int aX = pX;
        int aY = pY;

        // draw background
        for (; pY < mY; pY++) {
            for (pX = 0; pX < mX; pX++) {
                walls = map.getWalls(pX, pY);
                if ( xpm != null ) {
                    int cX = ( pX < xpm.getWidth()) ? pX : xpm.getWidth() - 1;
                    int cY = ( pY < xpm.getHeight()) ? pY : xpm.getHeight() - 1;
                    g.setColor(xpm.getColor(cX,cY));
                }
                else {
                    g.setColor(colorMap.background);
                }
                cp.drawBackground(g, pX, pY, walls);
            }
        }

        if ((sX == gX) && (sY == gY)) {
            g.setColor(colorMap.goal);
        } else {
            g.setColor(colorMap.resolved_path);
            cp.drawDot(g, new Position(gX, gY), pX, pY, mX, mY);
            g.setColor(colorMap.path);
        }
        cp.drawDot(g, new Position(sX, sY), pX, pY, mX, mY);

        pX = aX;
        pY = aY;

        // draw all walls within clip bounds horiz first then lines
        g.setColor(colorMap.wall);
        for (; pY < mY; pY++) {
            for (pX = 0; pX < mX; pX++) {
                walls = map.getWalls(pX, pY);
                cp.drawWalls(g, pX, pY, walls);
            }
        }

        pX = aX;
        pY = aY;

        synchronized (lockChange) {
            g.setColor(colorMap.goal);
            if (current != null) {
                cp.drawDot(g, current, pX, pY, mX, mY);
            }
            if (solvedPath != null) {
                for (DirectionPosition resolved : solvedPath) {
                    // cp.drawDot(g, resolved.getPosition(), pX, pY, mX, mY);
                    cp.drawPath(g, resolved, pX, pY, mX, mY);
                }
            }
        }

        if (this.showAll) {
            statusListener.addStatus("*");
            pX = aX;
            pY = aY;

            // draw all path within clip bounds horiz first then lines
            for (; pY < mY; pY++) {
                for (pX = 0; pX < mX; pX++) {
                    path = map.getPath(pX, pY);
                    if ((path & LabyModel.SOLVED) == LabyModel.SOLVED) {
                        g.setColor(colorMap.resolved_path);
                    } else {
                        g.setColor(colorMap.path);
                    }
                    if (path != 0) {
                        DirectionPosition dp = new DirectionPosition(path, new Position(pX, pY));
                        cp.drawPath(g, dp, pX, pY, pX + 1, pY + 1);
                    }
                }
            }
        }
    }


    public boolean notifySearch(DirectionPosition pPosition) {
        synchronized (lockChange) {
            current = pPosition.getPosition();
        }
        // should redraw ...
        invalidate();
        repaint();
        return true;

    }

    public void notifySearchError(String error) {
        System.err.println(error);
    }

    public void notifyCompletion(LinkedList<DirectionPosition> solvedPath) {
        LinkedList<DirectionPosition> newPath = new LinkedList<>(solvedPath);
        statusListener.addStatus("resolution completed");
        synchronized (lockChange) {
            this.solvedPath = newPath;
        }
        // should redraw ...
        invalidate();
        repaint();
    }

    public void resetResolution() {
        solvedPath = null;
    }

    @Override
    public Dimension getPreferredScrollableViewportSize() {
        return cp.getDimension();
    }

    @Override
    public int getScrollableUnitIncrement(Rectangle rectangle, int i, int i1) {
        return ( i == SwingConstants.VERTICAL ) ? (int)  cp.getHeight() : (int) cp.getWidth();
    }

    @Override
    public int getScrollableBlockIncrement(Rectangle rectangle, int i, int i1) {
        return ( i == SwingConstants.VERTICAL ) ? (int)  cp.getHeight() : (int) cp.getWidth();
    }

    @Override
    public boolean getScrollableTracksViewportWidth() {
        return false;
    }

    @Override
    public boolean getScrollableTracksViewportHeight() {
        return false;
    }
}
