package org.artisanlogiciel.games.maze.gui;

import org.artisanlogiciel.games.maze.*;
import org.artisanlogiciel.games.maze.model.LabyModelProvider;
import org.artisanlogiciel.games.maze.model.WidthHeightProvider;
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 CellGridComponent
        implements MazeCreationListener,
        MazeResolutionListener,
        MouseMotionListener,
        Scrollable
{
    private static final long serialVersionUID = 3163272907991176390L;
    private boolean viewBackground = true;

    LabyModelProvider map;
    Position current = null;
    LinkedList<DirectionPosition> solvedPath = null;
    LinkedList<DirectionPosition> drawingPath = null;
    final Object lockChange = new Object();
    // current position of human resolving
    Position human = new Position(0,0);

    // goal exit
    Position goal = new Position(-1,-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 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(), path);
                } else {
                    map.setDirection(last.getPosition(), 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 (human.equals(goal)) {
            statusListener.addStatus("Exit found by human !");
        }
    }

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

    void goTo(short direction)
    {
        resetResolution();
        if (map.moveInDirection(human, direction)) {
            proceed();
        }
    }

    /**
     * width, height of one cell,
     * offsetX, offsetY of upper left corner
     **/
    // public MazeComponent(WallsProvider map, MazeCellParameters cp)
    public MazeComponent(LabyModel map, WidthHeightProvider frame, StatusListener statusListener) {
        super();
        this.map = map;
        goal = new Position(map.getWidth() - 1,map.getHeight() - 1);
        resetCellRenderer(false,frame);
        this.statusListener = statusListener;
        addMouseMotionListener(this);
    }

    public void resetCellRenderer(boolean hexagon, WidthHeightProvider frame)
    {
        cp = createCellRenderer(hexagon,map,frame);
        setPreferredSize(cp.getDimension());
    }

    // public void resetWallsProvider(WallsProvider map)
    public void resetWallsProvider(LabyModel map) {
        this.map = map;
        solvedPath = null;
        // could be kept
        drawingPath = null;
        human = new Position(0,0);
        cp.resetMazeWidthHeight(map);
        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 !");
            }
        }
        */
    }

    public void setViewBackground(boolean viewBackground)
    {
        this.viewBackground = viewBackground;
    }
    private boolean hasBackground()
    {
        return viewBackground && (xpm != null);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        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();

        // visible part of grid :
        //     (min)-------------
        //       |                |
        //       _______________(max)

        Position min = new Position(
                (int) ((double) (r.getX() - cp.getOffsetX()) / cp.getWidth()),
                (int) ((double) (r.getY() - cp.getOffsetY()) / cp.getHeight())
        );

        min.limitToMin(0,0);

        if ( (min.getX() >= map.getWidth()) || (min.getY() >= map.getHeight()) )
            return;

        Position extent = new Position(
                (int) ((double) r.getWidth() / cp.getWidth()),
                (int) ((double) r.getHeight() / cp.getHeight())
        );
        Position max = min.translate(extent);

        max.limitToMax(map.getWidth(), map.getHeight());

        int mX = max.getX();
        int mY = max.getY();


        // draw background
        XYGridIterator iterator = new XYGridIterator(min,max);
        Position cell = null;
        while ( ( cell = iterator.next())  != null )
        {
            walls = map.getWalls(cell);
            if ( hasBackground() ) {
                Position colored = new Position(cell);
                colored.limitToMax(xpm.getWidth() - 1, xpm.getHeight() -1 );
                g.setColor(xpm.getColor(colored.getX(), colored.getY()));
            }
            else {
                g.setColor(colorMap.background);
            }
            cp.drawBackground(g, cell, walls);
        }

        if (human.equals(goal)) {
            g.setColor(colorMap.goal);
        } else {
            g.setColor(colorMap.resolved_path);
            cp.drawDot(g, goal, min, max);
            g.setColor(colorMap.path);
        }
        cp.drawDot(g, human, min, max);

        // draw all walls within clip bounds horiz first then lines
        g.setColor(colorMap.wall);
        iterator.reset();
        cell = null;
        while ( ( cell = iterator.next())  != null )
        {
            cp.drawWalls(g, cell, map.getWalls(cell));
        }

        if (this.showAll) {
            statusListener.addStatus("*");

            // draw all path within clip bounds horiz first then lines
            iterator.reset();
            cell = null;
            while ( ( cell = iterator.next())  != null )
            {
                path = map.getPath(cell.getX(), cell.getY());
                g.setColor(colorMap.path);
                if (path != 0) {
                    DirectionPosition dp = new DirectionPosition(path, new Position(cell));
                    cp.drawPath(g, dp, cell.getX(), cell.getY(), cell.getX() + 1, cell.getY() + 1);
                }
            }
        }

        synchronized (lockChange) {
            g.setColor(colorMap.goal);
            if (current != null) {
                cp.drawDot(g, current, min, max);
            }
            g.setColor(colorMap.resolved_path);
            if (solvedPath != null) {
                for (DirectionPosition resolved : solvedPath) {
                    cp.drawDot(g, resolved.getPosition(), min, max);
                    cp.drawPath(g, resolved, min.getX(), min.getY(), mX, mY);
                }
            }
        }

    }


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

    }

    public void notifySearchError(String error) {
        notifySearchInfoLevel(0,error);
    }

    public void notifySearchInfoLevel(int infoLevel, String pInfo)
    {
        statusListener.addStatus(pInfo);
    }


    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;
    }
}
