/*
 *  Copyright (C) 2014  Alfons Wirtz  
 *   website www.freerouting.net
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License at <http://www.gnu.org/licenses/> 
 *   for more details.
 *
 * BoardPanel.java
 *
 * Created on 3. Oktober 2002, 18:47
 */
package net.freerouting.freeroute;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import net.freerouting.freeroute.interactive.BoardHandling;
import net.freerouting.freeroute.interactive.ScreenMessages;

/**
 *
 * Panel containing the graphical representation of a routing board.
 *
 * @author Alfons Wirtz
 */
@SuppressWarnings("serial")
public final class BoardPanel extends javax.swing.JPanel {

    public javax.swing.JPopupMenu popup_menu_insert_cancel;
    public PopupMenuCopy popup_menu_copy;
    public PopupMenuMove popup_menu_move;
    public javax.swing.JPopupMenu popup_menu_corneritem_construction;
    public javax.swing.JPopupMenu popup_menu_main;
    public PopupMenuDynamicRoute popup_menu_dynamic_route;
    public PopupMenuStitchRoute popup_menu_stitch_route;
    public javax.swing.JPopupMenu popup_menu_select;
    public final ScreenMessages screen_messages;
    public final BoardFrame board_frame;
    BoardHandling board_handling = null;
    private final JScrollPane scroll_pane;
    Point2D right_button_click_location = null;
    private java.awt.Robot robot;
    private java.awt.Point middle_drag_position = null;
    /**
     * Defines the appearance of the custom custom_cursor in the board panel.
     * Null, if the standard custom_cursor is used.
     */
    private Cursor custom_cursor = null;

    /**
     * Creates a new BoardPanel in an Application
     */
    BoardPanel(ScreenMessages p_screen_messages, BoardFrame p_board_frame) {
        screen_messages = p_screen_messages;
        try {
            // used to be able to change the location of the mouse pointer
            robot = new java.awt.Robot();
        } catch (java.awt.AWTException e) {
            Logger.getLogger(BoardPanel.class.getName()).log(Level.INFO, "unable to create robot");
        }
        board_frame = p_board_frame;
        this.scroll_pane = board_frame.scroll_pane;
        default_init();
    }

    private void default_init() {
        setLayout(new java.awt.BorderLayout());

        setBackground(new java.awt.Color(0, 0, 0));
        setMaximumSize(new java.awt.Dimension(30_000, 20_000));
        setMinimumSize(new java.awt.Dimension(90, 60));
        setPreferredSize(new java.awt.Dimension(1_200, 900));
        addMouseMotionListener(new java.awt.event.MouseMotionAdapter() {
            @Override
            public void mouseDragged(java.awt.event.MouseEvent evt) {
                mouse_dragged_action(evt);
            }

            @Override
            public void mouseMoved(java.awt.event.MouseEvent evt) {
                mouse_moved_action(evt);
            }
        });
        addKeyListener(new java.awt.event.KeyAdapter() {
            @Override
            public void keyTyped(java.awt.event.KeyEvent evt) {
                board_handling.key_typed_action(evt.getKeyChar());
            }
        });
        addMouseListener(new java.awt.event.MouseAdapter() {
            @Override
            public void mouseClicked(java.awt.event.MouseEvent evt) {
                mouse_clicked_action(evt);
            }

            @Override
            public void mousePressed(java.awt.event.MouseEvent evt) {
                mouse_pressed_action(evt);
            }

            @Override
            public void mouseReleased(java.awt.event.MouseEvent evt) {
                board_handling.button_released();
                middle_drag_position = null;
            }
        });
        addMouseWheelListener((java.awt.event.MouseWheelEvent evt) -> {
            board_handling.mouse_wheel_moved(evt.getWheelRotation());
        });
        board_handling = BoardHandling.getInstance(this);
        setAutoscrolls(true);
        this.setCursor(new java.awt.Cursor(java.awt.Cursor.CROSSHAIR_CURSOR));
    }

    void create_popup_menus() {
        popup_menu_main = new PopupMenuMain(this.board_frame);
        popup_menu_dynamic_route = new PopupMenuDynamicRoute(this.board_frame);
        popup_menu_stitch_route = new PopupMenuStitchRoute(this.board_frame);
        popup_menu_corneritem_construction = new PupupMenuCornerItemConstruction(this.board_frame);
        popup_menu_select = new PopupMenuSelectedItems(this.board_frame);
        popup_menu_insert_cancel = new PopupMenuInsertCancel(this.board_frame);
        popup_menu_copy = new PopupMenuCopy(this.board_frame);
        popup_menu_move = new PopupMenuMove(this.board_frame);
    }

    public void zoom_with_mouse_wheel(java.awt.geom.Point2D p_point, int p_wheel_rotation) {
        if (this.middle_drag_position != null || p_wheel_rotation == 0) {
            return; // scrolling with the middle mouse butten in progress
        }
        double zoom_factor = 1 - 0.1 * p_wheel_rotation;
        zoom_factor = Math.max(zoom_factor, 0.5);
        zoom(zoom_factor, p_point);
    }

    private void mouse_pressed_action(java.awt.event.MouseEvent evt) {
        if (evt.getButton() == 1) {
            board_handling.mouse_pressed(evt.getPoint());
        } else if (evt.getButton() == 2 && middle_drag_position == null) {
            middle_drag_position = new java.awt.Point(evt.getPoint());
        }
    }

    private void mouse_dragged_action(java.awt.event.MouseEvent evt) {
        if (middle_drag_position != null) {
            scroll_middle_mouse(evt);
        } else {
            board_handling.mouse_dragged(evt.getPoint());
            scroll_near_border(evt);
        }
    }

    private void mouse_moved_action(java.awt.event.MouseEvent p_evt) {
        this.requestFocusInWindow(); // to enable keyboard aliases
        if (board_handling != null) {
            board_handling.mouse_moved(p_evt.getPoint());
        }
        if (this.custom_cursor != null) {
            this.custom_cursor.set_location(p_evt.getPoint());
            this.repaint();
        }
    }

    private void mouse_clicked_action(java.awt.event.MouseEvent evt) {
        if (evt.getButton() == 1) {
            board_handling.left_button_clicked(evt.getPoint());
        } else if (evt.getButton() == 3) {
            JPopupMenu curr_menu = board_handling.get_current_popup_menu();
            if (curr_menu != null) {
                int curr_x = evt.getX();
                int curr_y = evt.getY();
                curr_menu.show(this, curr_x, curr_y);
            }
            right_button_click_location = evt.getPoint();
        }
    }

    /**
     * overwrites the paintComponent method to draw the routing board
     */
    @Override
    public void paintComponent(Graphics p_g) {
        super.paintComponent(p_g);
        if (board_handling != null) {
            board_handling.draw(p_g);
        }
        if (this.custom_cursor != null) {
            this.custom_cursor.draw(p_g);
        }
    }

    /**
     * Returns the position of the viewport
     */
    public java.awt.Point get_viewport_position() {
        JViewport viewport = scroll_pane.getViewport();
        return viewport.getViewPosition();
    }

    /**
     * Sets the position of the viewport
     */
    void set_viewport_position(java.awt.Point p_position) {
        JViewport viewport = scroll_pane.getViewport();
        viewport.setViewPosition(p_position);
    }

    /**
     * zooms in at p_position
     */
    public void zoom_in(java.awt.geom.Point2D p_position) {
        zoom_in_out(p_position, Zoom.IN);
    }

    /**
     * zooms out at p_position
     */
    public void zoom_out(java.awt.geom.Point2D p_position) {
        zoom_in_out(p_position, Zoom.OUT);
    }

    /**
     * zooms in/out at p_position
     */
    private void zoom_in_out(java.awt.geom.Point2D p_position, Zoom zoom) {
        zoom(zoom.get_factor(), p_position);
    }

    /**
     * zooms in/out at right_button_click_location
     */
    void zoom_in_out(Zoom zoom) {
        zoom_in_out(right_button_click_location, zoom);
    }

    /**
     * zooms to frame
     */
    public void zoom_frame(java.awt.geom.Point2D p_position1, java.awt.geom.Point2D p_position2) {
        double width_of_zoom_frame = Math.abs(p_position1.getX() - p_position2.getX());
        double height_of_zoom_frame = Math.abs(p_position1.getY() - p_position2.getY());

        double center_x = Math.min(p_position1.getX(), p_position2.getX()) + (width_of_zoom_frame / 2);
        double center_y = Math.min(p_position1.getY(), p_position2.getY()) + (height_of_zoom_frame / 2);

        Point2D center_point = new Point2D.Double(center_x, center_y);

        Rectangle2D display_rect = get_viewport_bounds();

        double width_factor = display_rect.getWidth() / width_of_zoom_frame;
        double height_factor = display_rect.getHeight() / height_of_zoom_frame;

        java.awt.geom.Point2D changed_location = zoom(Math.min(width_factor, height_factor), center_point);
        set_viewport_center(changed_location);
    }

    void center_display() {
        center_display(right_button_click_location);
    }

    public void center_display(Point2D p_new_center) {
        java.awt.Point delta = set_viewport_center(p_new_center);
        java.awt.geom.Point2D new_center = get_viewport_center();
        java.awt.Point new_mouse_location
                = new java.awt.Point((int) (new_center.getX() - delta.getX()), (int) (new_center.getY() - delta.getY()));
        move_mouse(new_mouse_location);
        repaint();
        this.board_handling.logfile.start_scope(net.freerouting.freeroute.interactive.LogfileScope.CENTER_DISPLAY);
        net.freerouting.freeroute.geometry.planar.FloatPoint curr_corner = new net.freerouting.freeroute.geometry.planar.FloatPoint(p_new_center.getX(), p_new_center.getY());
        this.board_handling.logfile.add_corner(curr_corner);
    }

    public java.awt.geom.Point2D get_viewport_center() {
        java.awt.Point pos = get_viewport_position();
        Rectangle2D display_rect = get_viewport_bounds();
        return new java.awt.geom.Point2D.Double(pos.getX() + display_rect.getCenterX(), pos.getY() + display_rect.getCenterY());
    }

    /**
     * zooms the content of the board by p_factor Returns the change of the
     * cursor location
     */
    public java.awt.geom.Point2D zoom(double p_factor, java.awt.geom.Point2D p_location) {
        final int max_panel_size = 10_000_000;
        Dimension old_size = this.getSize();
        java.awt.geom.Point2D old_center = get_viewport_center();

        if (p_factor > 1 && Math.max(old_size.getWidth(), old_size.getHeight()) >= max_panel_size) {
            return p_location; // to prevent an sun.dc.pr.PRException, which I do not know, how to handle; maybe a bug in Java.
        }
        int new_width = (int) Math.round(p_factor * old_size.getWidth());
        int new_height = (int) Math.round(p_factor * old_size.getHeight());
        Dimension new_size = new Dimension(new_width, new_height);
        board_handling.graphics_context.change_panel_size(new_size);
        setPreferredSize(new_size);
        setSize(new_size);
        revalidate();

        java.awt.geom.Point2D new_cursor = new java.awt.geom.Point2D.Double(p_location.getX() * p_factor, p_location.getY() * p_factor);
        double dx = new_cursor.getX() - p_location.getX();
        double dy = new_cursor.getY() - p_location.getY();
        java.awt.geom.Point2D new_center = new java.awt.geom.Point2D.Double(old_center.getX() + dx, old_center.getY() + dy);
        java.awt.geom.Point2D adjustment_vector = set_viewport_center(new_center);
        repaint();
        java.awt.geom.Point2D adjusted_new_cursor = new java.awt.geom.Point2D.Double(new_cursor.getX() + adjustment_vector.getX() + 0.5, new_cursor.getY() + adjustment_vector.getY() + 0.5);
        return adjusted_new_cursor;
    }

    /**
     * Returns the viewport bounds of the scroll pane
     */
    Rectangle2D get_viewport_bounds() {
        return scroll_pane.getViewportBorderBounds();
    }

    /**
     * Sets the viewport center to p_point. Adjust the result, if p_point is
     * near the border of the viewport. Returns the adjustment vector
     */
    java.awt.Point set_viewport_center(java.awt.geom.Point2D p_point) {
        Rectangle2D display_rect = get_viewport_bounds();
        double x_corner = p_point.getX() - display_rect.getWidth() / 2;
        double y_corner = p_point.getY() - display_rect.getHeight() / 2;
        Dimension panel_size = getSize();
        double adjusted_x_corner = Math.min(x_corner, panel_size.getWidth());
        adjusted_x_corner = Math.max(adjusted_x_corner, 0);
        double adjusted_y_corner = Math.min(y_corner, panel_size.getHeight());
        adjusted_y_corner = Math.max(adjusted_y_corner, 0);
        java.awt.Point new_position = new java.awt.Point((int) adjusted_x_corner, (int) adjusted_y_corner);
        set_viewport_position(new_position);
        java.awt.Point adjustment_vector
                = new java.awt.Point((int) (adjusted_x_corner - x_corner), (int) (adjusted_y_corner - y_corner));
        return adjustment_vector;
    }

    /**
     * Selects the p_signal_layer_no-th layer in the select_parameter_window.
     */
    public void set_selected_signal_layer(int p_signal_layer_no) {
        if (board_frame.savable_subwindows != null) {
            ((WindowSelectParameter) board_frame.savable_subwindows.get(SavableSubwindowKey.SELECT_PARAMETER))
                    .select(p_signal_layer_no);
            this.popup_menu_dynamic_route.disable_layer_item(p_signal_layer_no);
            this.popup_menu_stitch_route.disable_layer_item(p_signal_layer_no);
            this.popup_menu_copy.disable_layer_item(p_signal_layer_no);
        }
    }

    void init_colors() {
        board_handling.graphics_context.item_color_table.addTableModelListener(new ColorTableListener());
        board_handling.graphics_context.other_color_table.addTableModelListener(new ColorTableListener());
        setBackground(board_handling.graphics_context.get_background_color());
    }

    private void scroll_near_border(java.awt.event.MouseEvent p_evt) {
        final int border_dist = 50;
        Rectangle2D r
                = new Rectangle2D.Double(p_evt.getX() - border_dist, p_evt.getY() - border_dist, 2 * border_dist, 2 * border_dist);
        ((JComponent) p_evt.getSource()).scrollRectToVisible(r.getBounds());
    }

    private void scroll_middle_mouse(java.awt.event.MouseEvent p_evt) {
        double delta_x = middle_drag_position.x - p_evt.getX();
        double delta_y = middle_drag_position.y - p_evt.getY();

        java.awt.Point view_position = get_viewport_position();

        double x = (view_position.x + delta_x);
        double y = (view_position.y + delta_y);

        Dimension panel_size = this.getSize();
        x = Math.min(x, panel_size.getWidth() - this.get_viewport_bounds().getWidth());
        y = Math.min(y, panel_size.getHeight() - this.get_viewport_bounds().getHeight());

        x = Math.max(x, 0);
        y = Math.max(y, 0);

        java.awt.Point p = new java.awt.Point((int) x, (int) y);
        set_viewport_position(p);
    }

    void move_mouse() {
        move_mouse(right_button_click_location);
    }

    public void move_mouse(Point2D p_location) {
        if (robot == null) {
            return;
        }
        java.awt.Point absolute_panel_location = board_frame.absolute_panel_location();
        java.awt.Point view_position = get_viewport_position();
        int x = (int) Math.round(absolute_panel_location.getX() - view_position.getX() + p_location.getX()) + 1;
        int y = (int) Math.round(absolute_panel_location.getY() - view_position.getY() + p_location.getY() + 1);
        robot.mouseMove(x, y);
    }

    /**
     * If p_value is true, the custom crosshair cursor will be used in display.
     * Otherwise the standard Cursor will be used. Using the custom cursor may
     * slow down the display performance a lot.
     */
    public void set_custom_crosshair_cursor(boolean p_value) {
        if (p_value) {
            this.custom_cursor = Cursor.get_45_degree_cross_hair_cursor();
        } else {
            this.custom_cursor = null;
        }
        board_frame.refresh_windows();
        repaint();
    }

    /**
     * If the result is true, the custom crosshair cursor will be used in
     * display. Otherwise the standard Cursor will be used. Using the custom
     * cursor may slow down the display performance a lot.
     */
    public boolean is_custom_cross_hair_cursor() {
        return this.custom_cursor != null;
    }

    private class ColorTableListener implements TableModelListener {

        @Override
        public void tableChanged(TableModelEvent p_event) {
            //redisplay board because some colors have changed.
            setBackground(board_handling.graphics_context.get_background_color());
            repaint();
        }
    }
}
