package shapes77.canvas_4;
/* UTF-8 codepage: Příliš žluťoučký kůn úpěl ďábelské ó. ÷ × ¤  */

import shapes77.util.IO;
import shapes77.util.NamedColor;

import javax.swing.*;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.util.Properties;


/*******************************************************************************
 * Instance třídy {@code Canvas} (jedináček) slouží jako virtuální plátno,
 * na něž mohou být kresleny jednotlivé obrazce.
 * <p>
 * Třída neposkytuje veřejný konstruktor,
 * protože chce, aby její instance byla jedináček,
 * tj. aby se všechno kreslilo na jedno a to samé plátno.
 * Jediným způsobem, jak získat odkaz na instanci třídy Plátno,
 * je volaní statické metody {@link #getInstance()}.</p>.
 * <p>
 * Aby bylo možno na plátno obyčejné kreslit
 * a nebylo nutno kreslené objekty přihlašovat,
 * odmazané části obrazců se automaticky neobnovují.
 * Je-li proto při smazání některého obrazce odmazána část jiného obrazce,
 * je třeba příslušný obrazec explicitně překreslit.</p>
 */
public final class Canvas
{
//\CC== CLASS CONSTANTS (CONSTANT CLASS/STATIC ATTRIBUTES/FIELDS) ==============

    /** Implicitní velikost kroku - standardu plátna. */
    public static final int DEFAULT_STEP = 50;

    /** Maximální povolená velikost kroku - standardu plátna. */
    public static final int MAX_STEP = 200;

    /** Relativní cesta k souboru s konfiguračními detaily -
     *  např. s počáteční pozicí plátna na obrazovce.
     *  Cesta je zadána relativně k uživatelské složce/adresáři. */
    private static final String CONFIG_PATH = "bluej/win_settings.properties";

    /** Výchozí titulek v záhlaví okna plátna. */
    private static final String TITLE  = "Jednoduché plátno";

    /** Počáteční šířka plátna v bodech. */
    public static final int WIDTH_0 = 300;

    /** Počáteční výška plátna v bodech. */
    public static final int HEIGHT_0 = 300;

    /** Počáteční barva pozadí plátna. */
    public static final NamedColor BACKGROUND_0 = NamedColor.CREAMY;

    /** Implicitní počáteční bodová pozice okna na obrazovce. */
    public static final Point POINT_0 = new Point(0, 0);

    /** Kreslítko, jehož pomocí je možno na plátno kreslit. */
//        private static final Painter PAINTER = new Painter();



//\CV== CLASS VARIABLES (VARIABLE CLASS/STATIC ATTRIBUTES/FIELDS) ==============

    /** Jediná instance této třídy */
    private static volatile Canvas SINGLETON;

    /** Přepravka s inicializačními informacemi zahrnujícími
     *  pozici aplikačního okna na displeji a velikostí jeho rámečků.
     *  Při použití více monitorů je občas potřeba
     *  pozici okna po jeho zviditelnění aktualizovat.
     *  Atribut je definován jako statický, aby jej bylo možno nastavit
     *  ještě před vytvořením instance plátna. */
    private static InitCrate initCrate;

    /** Příznak dokončení inicializace. */
    private static volatile boolean initialized = false;

    /** Základní vzdálenost na plátně v pixelech */
    private static int step = DEFAULT_STEP;



//##############################################################################
//\CI== CLASS (STATIC) INITIALIZER (CLASS CONSTRUCTOR) =========================
//\CF== CLASS (STATIC) FACTORY METHODS =========================================

    /***************************************************************************
     * Jediná metoda umožnující získat odkaz na instanci plátna.
     * Protože je však tato instance definována jako jedináček
     * Vrací metoda pokaždé odkaz na stejnou instanci.
     *
     * @return Odkaz na instanci třídy Plátno.
     */
    public static Canvas getInstance()
    {
        if (SINGLETON == null) {    //)! initialized) {
            synchronized(Canvas.class) {
                if (SINGLETON == null) {   //! initialized) {
                    initialize();   //Including SINGLETON creation
                }
            }
        }
        SINGLETON.setVisible(true);     //Holder.CANVAS.setVisible(true);
        return SINGLETON;               //Holder.CANVAS;
    }



//\CG== CLASS (STATIC) GETTERS AND SETTERS =====================================

    /***************************************************************************
     * Vrátí implicitní velikost implicitní vzdálenosti (kroku),
     * o kterou se instance přesune při volaní bezparametrických metod přesunu.
     *
     * @return Implicitní velikost implicitní vzdálenosti (kroku) v bodech
     */
    public static int getDefaultStep()
    {
        return DEFAULT_STEP;
    }


    /***************************************************************************
     * Nastaví implicitní vzdálenost (krok), o kterou se instance přesune
     * při volaní bezparametrických metod přesunu na implicitní velikost.
     */
    public static void setStepToDefault()
    {
        Canvas.step = DEFAULT_STEP;
    }


    /***************************************************************************
     * Vrátí implicitní vzdálenost (krok), o kterou se instance přesune
     * při volaní bezparametrických metod přesunu.
     *
     * @return Velikost implicitního kroku v bodech
     */
    public static int getStep()
    {
        return step;
    }


    /***************************************************************************
     * Nastaví implicitní vzdálenost (krok), o kterou se instance přesune
     * při volaní bezparametrických metod přesunu.
     *
     * @param size  Velikost implicitního kroku v bodech;<br>
     *              musí platit:  0 &lt;= velikost &lt;= {@link #MAX_STEP}
     */
    public static void setStep(int size)
    {
        if ((size <= 0)  ||  (size > MAX_STEP)) {
            throw new IllegalArgumentException(
                "\nKrok musí být z intevalu <1; " + MAX_STEP + ">.");
        }
        Canvas.step = size;
    }


    /***************************************************************************
     * Nastaví novou pozici plátna zadáním jeho nových souřadnic.
     *
     * @param  x Nova vodorovná souřadnice okna plátna
     * @param  y Nová svislá souřadnice okna plátna
     */
    public static void placeAt(int x, int y)
    {
        SINGLETON.setPosition(x, y);
    }



//\CM== CLASS (STATIC) REMAINING NON-PRIVATE METHODS ===========================

    /***************************************************************************
     * Smaže plátno, přesněji smaže všechny obrazce na plátně.
     * Tato metoda by měla být definována jako metoda instance,
     * avšak protože je instance jedináček,
     * byla metoda pro snazší dostupnost definovaná jako metoda třídy,
     * aby nebylo potřeba před žádostí o smazání plátna vytvářet jeho instanci.
     */
    static public void clearCanvas()
    {
        SINGLETON.clear();      //Holder.CANVAS.clear();
    }


    /***************************************************************************
     * Zobrazí okno plátna nad ostatními okny.
     */
    public static void showCanvas()
    {
        getInstance().setVisible(true);
    }



//\CP== CLASS (STATIC) PRIVATE AND AUXILIARY METHODS ===========================

    /***************************************************************************
     * Inicializuje některé parametry z konfiguračního souboru.
     * Tento soubor je umístěn v domovském adresáři uživatele
     * ve složce {@code .rup} v souboru {@code bluej.properties}.
     * Je určen především pro učitele, aby jim usnadnil umisťování oken
     * při práci s několika monitory, z nichž pouze jeden vidí studenti.
     *
     * @return Počáteční pozice aplikačního okna na displeji
     */
    private static void configurationFromFile()
    {
        Properties sysProp = System.getProperties();
        String     userDir = sysProp.getProperty("user.home");
        File       rupFile = new File(userDir, CONFIG_PATH);
        Properties rupProp = new Properties();
        try {
            try (Reader reader = new FileReader(rupFile)) {
                rupProp.load(reader);
            }
            initCrate = new InitCrate(xyPropery(rupProp, ""),
                                      xyPropery(rupProp, "d"));
        }catch(IOException | NumberFormatException e)  {
            initCrate = new InitCrate(POINT_0, POINT_0);
        }
    }


    /***************************************************************************
     * Inicializuje aplikační okno plátna.
     * Metoda musí být volána z {@code AWT Event Queue}.
     */
    @SuppressWarnings("serial")
    private static void initialize()
    {
        configurationFromFile();

        Runnable prepareCanvas = new Runnable() {
            @Override public void run()
            {
                SINGLETON = new Canvas();   //Holder.CANVAS = new Canvas();
            }
        };
        try {
            java.awt.EventQueue.invokeAndWait(prepareCanvas);
        } catch (InterruptedException | InvocationTargetException ex) {
            StringWriter sw = new StringWriter();
            PrintWriter  pw = new PrintWriter(sw);

            sw.write("\nCreation of CanvasManager didn't succeed\n");
            ex.printStackTrace(pw);

            String msg = sw.toString();
            System.err.println(msg);
            IO.inform(msg);

            System.exit(1);
        }

        //Canvas is made, we will place the dialogs
        int x = SINGLETON.appWindow.getX();
        int y = SINGLETON.appWindow.getY()
              + SINGLETON.appWindow.getHeight();
        IO.setDialogsPosition(x, y);

        //Everything is ready, we may initialize
        initialized = true;

    }


    /***************************************************************************
     * Přečte ze zadaných vlastností hodnotu vodorovné a svislé souřadnice
     * se zadaným prefixem v názvu před "x" a "y" a vrátí údaje
     * jako souřadnice instance třídy {@link Point}.
     *
     * @param rupProp   Mapa s načtenými vlastnostmi
     * @param prefix    Prefix v názvu vlastností před "x" a "y"
     * @return Načtené údaje jako souřadnice instance třídy {@link Point}
     * @throws NumberFormatException Nepodaří-li se převést text na číslo
     */
    private static Point xyPropery(Properties rupProp, String prefix)
        throws NumberFormatException
    {
        Point position;
        String sx = rupProp.getProperty("canvas." + prefix + "x");
        String sy = rupProp.getProperty("canvas." + prefix + "y");
        int x = Integer.parseInt(sx.trim());
        int y = Integer.parseInt(sy.trim());
        position = new Point(x, y);
        return position;
    }



//##############################################################################
//\IC== INSTANCE CONSTANTS (CONSTANT INSTANCE ATTRIBUTES/FIELDS) ===============

        /** Aplikační okno plátna. */
        private final JFrame appWindow;

        /** Instance kontejneru, do nějž bude plátno vloženo. */
        private final JPanel canvasContainer;



//\IV== INSTANCE VARIABLES (VARIABLE INSTANCE ATTRIBUTES/FIELDS) ===============

   //Z venku neovlivnitelné Atributy pro zobrazeni plátna v aplikačním okně

        /** Vše se kreslí na obraz - ten se snadněji překreslí. */
        private Image canvasImage;

    /** Kreslítko, jehož pomocí je možno na plátno kreslit. */
       private Graphics2D painter;


    //Atributy přímo ovlivnitelné uživatelem

        /** Barva pozadí při kreslení. */
        private NamedColor backgroundColor = BACKGROUND_0;

        /** Šířka plátna v bodech. */
        private int width;

        /** Výška plátna v bodech. */
        private int height;



//##############################################################################
//\II== INSTANCE INITIALIZERS (CONSTRUCTORS) ===================================

    /***************************************************************************
     * Implicitní (a jediný) konstruktor.
     * Je volán pouze jednou, a to z metody {@link #initialize()}.
     *
     * @param position Počáteční pozice aplikačního okna
     */
    @SuppressWarnings("serial")     //Because of annonymous class
    private Canvas()
    {
        appWindow = new JFrame();
        appWindow.setLocation(initCrate.point);
        appWindow.setTitle(TITLE);

        //By closing the window we close the whole application
        appWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        canvasContainer = new JPanel()
        {   /** The obligatorily overridden method
             * of the class {@link JPanel}. */
            @Override
            public void paintComponent(Graphics g) {
                g.drawImage(canvasImage, 0, 0, null);
            }
        };
        appWindow.setContentPane(canvasContainer);

        setSizePrivate(WIDTH_0, HEIGHT_0); //Prepares and paints an empty window
//        IO.Correction.windowLocation(appWindow);
//        prepareImage();
//        clear();

        IO.setDialogsPosition(initCrate.point.x,
                              initCrate.point.y + appWindow.getSize().height);
    }



//\IA== INSTANCE ABSTRACT METHODS ==============================================
//\IG== INSTANCE GETTERS AND SETTERS ===========================================

    /***************************************************************************
     * Vrátí aktuální barvu pozadí.
     *
     * @return   Nastavena barva pozadí
     */
    public NamedColor getBackgroundColor()
    {
        return backgroundColor;
    }


    /***************************************************************************
     * Nastaví pro plátno barvu pozadí.
     *
     * @param color  Nastavovaná barva pozadí
     */
    public void setBackgroundColor(NamedColor color)
    {
        this.backgroundColor = color;
        painter.setBackground(color.getAWTColor());
        clear();
    }


    /***************************************************************************
     * Vrátí text v titulkové liště okna plátna.
     *
     * @return Text v titulkové liště okna plátna
     */
    public String getTitle()
    {
        return appWindow.getTitle();
    }


    /***************************************************************************
     * Nastaví text v titulkové liště okna plátna.
     *
     * @param title  Nastavovaný text v titulkové liště okna plátna
     */
    public void setTitle(String title)
    {
        appWindow.setTitle(title);
    }


    /***************************************************************************
     * Vrátí vodorovnou souřadnici okna plátna.
     *
     * @return  Vodorovná souřadnice okna plátna
     */
    public int getX()
    {
        return appWindow.getX();
    }


    /***************************************************************************
     * Vrátí svislou souřadnici okna plátna.
     *
     * @return  Svislá souřadnice okna plátna
     */
    public int getY()
    {
        return appWindow.getY();
    }


    /***************************************************************************
     * Nastaví novou pozici plátna zadáním jeho nových souřadnic.
     *
     * @param  x Nova vodorovná souřadnice okna plátna
     * @param  y Nová svislá souřadnice okna plátna
     */
    public void setPosition(int x, int y)
    {
        appWindow.setLocation(x, y);
        setVisible(true);
    }


    /***************************************************************************
     * Vrátí šířku plátna.
     *
     * @return  Aktuální šířka plátna v bodech
     */
    public int getWidth()
    {
        return width;
    }


    /***************************************************************************
     * Vrátí výšku plátna.
     *
     * @return  Aktuální výška plátna v bodech
     */
    public int getHeight()
    {
        return height;
    }


    /***************************************************************************
     * Nastaví nový rozměr plátna zadáním jeho výsky a šířky.
     *
     * @param  width  Nova šířka plátna v bodech
     * @param  height Nová výška plátna v bodech
     */
    public void setSize(int width, int height)
    {
        setSizePrivate(width, height);
//        setVisible(true);
//        prepareImage();
//        clear();
    }


    /***************************************************************************
     * Poskytuje informaci o aktuální viditelnosti okna.
     * Nicméně i viditelná okna mohou být zakryta jinými okny.
     *
     * @return Je-li okno viditelné, vrátí {@code true},
     *         v opačném případě vrátí {@code false}
     */
    public boolean isVisible()
    {
        return appWindow.isVisible();
    }


    /***************************************************************************
     * Nastaví viditelnost plátna.
     *
     * @param visible {@code true} má-li být aplikační okno viditelné,
     *                jinak {@code false}
     */
    public void setVisible(boolean visible)
    {
        boolean change = (isVisible() != visible);
        if (change) {
            if (! visible) {
                appWindow.setVisible(false);
            }
            else {
                if (java.awt.EventQueue.isDispatchThread()) {
                    setVisibleInternal();
                    return;
                }
                Runnable run = new Runnable() {
                    @Override
                    public void run()
                    {
                        setVisibleInternal();
                    }
                };
                try {
                    java.awt.EventQueue.invokeAndWait(run);
                }
                catch (Exception ex) {
                    throw new RuntimeException(
                            "\nException by visibilty setting", ex);
                }
            }
        }
    }



//\IM== INSTANCE REMAINING NON-PRIVATE METHODS =================================

    /***************************************************************************
     * Smaže plátno, přesněji smaže všechny obrazce na plátně.
     */
    public void clear()
    {
        erase(new Rectangle2D.Double(0, 0, width, height));
    }


    /***************************************************************************
     * Zadanou barvou nakreslí na plátno úsečku se zadanými krajními body.
     *
     * @param  x1    x-ová souřadnice počátku
     * @param  y1    y-ová souřadnice počátku
     * @param  x2    x-ová souřadnice konce
     * @param  y2    x-ová souřadnice konce
     * @param  color Barva úsečky
     */
    public void drawLine(int x1, int y1, int x2, int y2, NamedColor color)
    {
        setForegroundColor(color);
        painter.drawLine(x1, y1, x2, y2);
        canvasContainer.repaint();
    }


    /***************************************************************************
     * Vypíše na plátno zadaný text aktuálním písmem a zadanou barvou.
     *
     * @param text   Zobrazovaný text
     * @param x      x-ová souřadnice textu
     * @param y      y-ová souřadnice textu
     * @param color  Barva, kterou se zadaný text vypíše
     */
    public void drawString(String text, int x, int y, NamedColor color)
    {
        setForegroundColor(color);
        painter.drawString(text, x, y);
        canvasContainer.repaint();
    }


    /***************************************************************************
     * Smaže na plátně zadaný obrazec, tj. překreslí jej barvou pozadí.
     * Obrazec však nadále existuje, pouze již není vidět.
     *
     * @param shape Obrazec, který má být smazán
     */
    public void erase(Shape shape)
    {
        Color original = painter.getColor();
        painter.setColor(backgroundColor.getAWTColor());
        painter.fill(shape);
        painter.setColor(original);
        canvasContainer.repaint();
    }


    /***************************************************************************
     * Nakreslí zadaný obrazec a vybarví jej zadanou barvou.
     *
     * @param shape Kreslený obrazec
     * @param color Barva výplně
     */
    public void fill(Shape shape, NamedColor color)
    {
        setForegroundColor(color);
        painter.fill(shape);
        canvasContainer.repaint();
    }


    /***************************************************************************
     * Vrátí string reprezentující danou instanci (její textový podpis).
     * Obsahuje název třídy, pozici a rozměr okna a barvu pozadí.
     * Používá se především při ladění.
     *
     * @return Textový podpis dané instance
     */
    @Override
    public String toString()
    {
        return this.getClass().getName()
             + "(" + width + "x" + height
             + " na pozici ["
             + getX() + "; " + getY()
             + "], pozadi="
             + backgroundColor + ")";
    }



//\IP== INSTANCE PRIVATE AND AUXILIARY METHODS =================================

    /***************************************************************************
     * Připraví obrázek, do nějž se budou všechny tvary kreslit.
     */
    private void prepareImage()
    {
        canvasImage = canvasContainer.createImage(width, height);
        painter = (Graphics2D)canvasImage.getGraphics();
        painter.setColor(backgroundColor.getAWTColor());
        painter.fillRect(0, 0, width, height);
        painter.setColor(Color.BLACK);
    }


    /***************************************************************************
     * Nastaví barvu, kterou se bude kreslit.
     *
     * @param color Nastavovaná barva
     */
    private void setForegroundColor(NamedColor color)
    {
        painter.setColor(color.getAWTColor());
    }


    /***************************************************************************
     * Nastaví zadaný rozměr plátna, ale už nic jiného.
     * Soukromá verze určená pro konstruktor.
     * Veřejná verze přidává ještě zviditelnění plátna a přípravu obrázku.
     *
     * @param width  Nastavovaná bodová šířka plátna
     * @param height Nastavovaná bodová výška plátna
     */
    private void setSizePrivate(int width, int height)
    {
        this.width  = width;
        this.height = height;
        appWindow.setResizable(true);
        canvasContainer.setPreferredSize(new Dimension(width, height));
        appWindow.pack();
        Insets ins  = appWindow.getInsets();
        if (initCrate.inset.y == 0) {
            initCrate.inset.x = ins.left + ins.right;
            initCrate.inset.y = ins.top  + ins.bottom;
        }
        appWindow.setSize(width  + initCrate.inset.x,
                          height + initCrate.inset.y);
        appWindow.setResizable(false);    //Není možné měnit rozměr pomocí myši
        setVisible(true);
        prepareImage();
        clear();
    }


    /***************************************************************************
     * Metoda volaná z vlákna událostí.
     */
    private void setVisibleInternal()
    {
        initCrate.point = appWindow.getLocation();
        appWindow.setVisible(true);

        //With more displays the window doesn't work well - it is
        appWindow.setLocation(initCrate.point); //necessary to set again
        appWindow.setAlwaysOnTop(true);
        appWindow.toFront();
        appWindow.setAlwaysOnTop(false);
    }



//##############################################################################
//\NT== NESTED DATA TYPES ======================================================

////////////////////////////////////////////////////////////////////////////////
//\NC0 /////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

    /***************************************************************************
     * Instance třídy {@code InitCrate} jsou přepravky se základními
     * inicializačními informacemi týkajícími se aplikačního okna.
     */
    private static class InitCrate
    {
        /** Výchozí pozice aplikačního okna. */
        Point point;

        /** O kolik bude aplikační okno větší než plátno. */
        Point inset;

        /***********************************************************************
         * Vytvoří a inicializuje novou instanci.
         *
         * @param point Výchozí pozice aplikačního okna
         * @param inset O kolik bude aplikační okno větší než plátno
         */
        public InitCrate(Point point, Point inset)
        {
            this.point = point;
            this.inset = inset;
        }

    }

}
