package pipe.views;

import pipe.constants.GUIConstants;
import pipe.controllers.PetriNetController;
import uk.ac.imperial.pipe.exceptions.PetriNetComponentNotFoundException;
import uk.ac.imperial.pipe.models.petrinet.Place;
import uk.ac.imperial.pipe.models.petrinet.Token;

import javax.swing.event.MouseInputAdapter;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Graphical representation of a Place
 */
public class PlaceView extends ConnectableView<Place> {

    /**
     * Class logger
     */
    private static final Logger LOGGER = Logger.getLogger(PlaceView.class.getName());

    /**
     * Constructor
     * @param model underlying model
     * @param parent parent of the view
     * @param controller Petri net controller of the Petri net the model is housed in
     * @param placeHandler mouse input handler for describing how this view responds to mouse events
     */
    public PlaceView(Place model, Container parent, PetriNetController controller, MouseInputAdapter placeHandler) {
        super(model.getId(), model, controller, controller.getPlaceController(model), parent, new Ellipse2D.Double(-model.getWidth()/2, -model.getHeight()/2, model.getWidth(), model.getHeight()));
        setChangeListener();
        setMouseListener(placeHandler);

        Rectangle bounds = shape.getBounds();
        Rectangle newBounds = new Rectangle((int)(model.getCentre().getX() + bounds.getX()), (int)(model.getCentre().getY() + bounds.getY()), (int) bounds.getWidth() + getComponentDrawOffset(), (int)bounds.getHeight() + getComponentDrawOffset());
        setBounds(newBounds);
    }


    /**
     *
     * Sets the view mouse listeners
     *
     * @param placeHandler mouse input handler for describing how this view responds to mouse events
     */
    private void setMouseListener(MouseInputAdapter placeHandler) {
        this.addMouseListener(placeHandler);
        this.addMouseWheelListener(placeHandler);
        this.addMouseMotionListener(placeHandler);
    }

    /**
     * Listens for changes in the model
     * All changes cause a repaint
     */
    private void setChangeListener() {
        model.addPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
                repaint();
            }
        });
    }


    /**
     * Paints the Place component taking into account the n q12[umber of tokens from
     * the storeCurrentMarking
     *
     * @param g The PositionGraphics object onto which the Place is drawn.
     */
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g.create();

        Rectangle bounds = shape.getBounds();
        g2.translate(bounds.getWidth()/2, bounds.getHeight()/2);

        if (hasCapacity()) {
            g2.setStroke(new BasicStroke(2.0f));
            setToolTipText("k = " + model.getCapacity());
        } else {
            g2.setStroke(new BasicStroke(1.0f));
            setToolTipText("k = \u221E");
        }
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        if (isSelected() && !ignoreSelection) {
            g2.setColor(GUIConstants.SELECTION_FILL_COLOUR);
        } else {
            g2.setColor(GUIConstants.ELEMENT_FILL_COLOUR);
        }
        g2.fill(shape);

        if (isSelected() && !ignoreSelection) {
            g2.setPaint(GUIConstants.SELECTION_LINE_COLOUR);
        } else {
            g2.setPaint(GUIConstants.ELEMENT_LINE_COLOUR);
        }
        g2.draw(shape);

        g2.setStroke(new BasicStroke(1.0f));

        // Paints border round a tagged place - paint component is called after any action on the place, so this bit
        // of code doesn't have to be called specially

        if (this.isTagged()) {
            AffineTransform oldTransform = g2.getTransform();

            AffineTransform scaleTransform = new AffineTransform();
            scaleTransform.setToScale(1.2, 1.2);

            g2.transform(scaleTransform);

            g2.translate(-2, -2);

            g2.fill(shape);

            g2.translate(2, 2);

            g2.setTransform(oldTransform);
        }

        g2 = (Graphics2D) g.create();
        paintTokens(g2);

        g2.dispose();
    }

    /**
     * Displays tokens in the Place
     */
    private void paintTokens(Graphics2D g2) {
        int totalMarking = model.getNumberOfTokensStored();
        boolean displayTextualNumber = totalMarking > 5;
        if (displayTextualNumber) {
            displayTextualTokens(g2);
        } else {
            displayOvalTokens(g2);
        }

    }

    /**
     * Displays each token in the Place as an oval
     *
     * If the token does not exist in the petri net the color is displayed
     * as black
     *
     *
     * @param g2 graphics
     */
    private void displayOvalTokens(Graphics2D g2) {
        int offset = 0;

        Map<String, Integer> tokenCounts = model.getTokenCounts();
        for (Map.Entry<String, Integer> entry : tokenCounts.entrySet()) {
            String tokenId = entry.getKey();
            Integer count = entry.getValue();
            Insets insets = getInsets();
            Token token = null;
            try {
                token = petriNetController.getToken(tokenId);
                paintOvalTokens(g2, insets, token.getColor(), count, offset);
            } catch (PetriNetComponentNotFoundException e) {
                LOGGER.log(Level.SEVERE, e.getMessage());
                paintOvalTokens(g2, insets, Color.BLACK, count, offset);
            }
            offset += count;
        }
    }

    /**
     * Paints tokens as ovals on the place
     * Can only paint five tokens so tokenNumber represents which number the count is
     * starting at
     *
     * @param g2          graphics
     * @param insets      insets
     * @param color       colour of oval
     * @param count       number of token ovals to paint
     * @param tokenNumber token number
     */
    private void paintOvalTokens(Graphics2D g2, Insets insets, Color color, int count, int tokenNumber) {
        int x = insets.left;
        int y = insets.top;
        g2.setColor(color);
        int WIDTH = 4;
        int HEIGHT = 4;
        int position = tokenNumber;
        for (int i = 0; i < count; i++) {
            switch (position) {
                case 4:
                    g2.drawOval(x + 6, y + 6, WIDTH, HEIGHT);
                    g2.fillOval(x + 6, y + 6, WIDTH, HEIGHT);
                    break;
                case 3:
                    g2.drawOval(x + 18, y + 20, WIDTH, HEIGHT);
                    g2.fillOval(x + 18, y + 20, WIDTH, HEIGHT);
                    break;
                case 2:
                    g2.drawOval(x + 6, y + 20, WIDTH, HEIGHT);
                    g2.fillOval(x + 6, y + 20, WIDTH, HEIGHT);
                    break;
                case 1:
                    g2.drawOval(x + 18, y + 6, WIDTH, HEIGHT);
                    g2.fillOval(x + 18, y + 6, WIDTH, HEIGHT);
                    break;
                case 0:
                    g2.drawOval(x + 12, y + 13, WIDTH, HEIGHT);
                    g2.fillOval(x + 12, y + 13, WIDTH, HEIGHT);
                    break;
                default:
                    break;
            }
            position++;
        }
    }

    /**
     * Display each token in the place as a number textually
     *
     * If the token does not exist in the petri net the color is displayed
     * as black
     *
     * @param g2 graphics
     */
    private void displayTextualTokens(Graphics2D g2) {
        int offset = 0;

        Map<String, Integer> tokenCounts = model.getTokenCounts();
        for (Map.Entry<String, Integer> entry : tokenCounts.entrySet()) {
            String tokenId = entry.getKey();
            Integer count = entry.getValue();
            Insets insets = getInsets();
            try {
                Token token = petriNetController.getToken(tokenId);
                paintTextualTokens(g2, insets, token.getColor(), count, offset);
            } catch (PetriNetComponentNotFoundException e) {
                LOGGER.log(Level.SEVERE, e.getMessage());
                paintTextualTokens(g2, insets, Color.BLACK, count, offset);

            } offset += 10;
        }
    }

    /**
     * Paints tokens as a string representation of their number in the place
     *
     * @param g2     graphics
     * @param insets insets
     * @param color  color of token
     * @param count  number of tokens to represent
     * @param offset offset of textual representation relative to the place
     */
    private void paintTextualTokens(Graphics2D g2, Insets insets, Color color, int count, int offset) {
        int x = insets.left;
        int y = insets.top;
        g2.setColor(color);
        if (count > 999) {
            g2.drawString(String.valueOf(count), x, y + 10 + offset);
        } else if (count > 99) {
            g2.drawString(String.valueOf(count), x + 3, y + 10 + offset);
        } else if (count > 9) {
            g2.drawString(String.valueOf(count), x + 7, y + 10 + offset);
        } else if (count != 0) {
            g2.drawString(String.valueOf(count), x + 12, y + 10 + offset);
        }
    }

    /**
     *
     * @return true if the underlying model has a capacity
     */
    private boolean hasCapacity() {
        return model.getCapacity() > 0;
    }


    /**
     *
     * @return if the place is tagged
     */
    private boolean isTagged() {
        return false;
    }

    /**
     * Adds the place id label to the container
     * @param container to add itself to
     */
    @Override
    public void addToContainer(Container container) {
        addLabelToContainer(container);
    }

}

