/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package expressionevaluation;

import expressiontree.ExpressionTreeElement;
import expressiontree.operands.NumberType;
import expressiontree.operators.IncompatibleNumberFormatException;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JProgressBar;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;
import org.apfloat.Apfloat;
import org.xml.sax.SAXException;
import xmlhandling.ExpressionLogicalException;
import xmlhandling.PrecisionIsTooLowException;
import xmlhandling.TreeBuilderFromXML;
import xmlhandling.UnknownOperatorException;
import xmlhandling.XMLBuilder;
import xmlhandling.XMLBuilderFromInfixText;
import xmlhandling.XMLBuilderFromPrefixText;

/**
 * A függvényrajzolást elvégző osztály.
 * @author Esta
 */
public class FunctionDrawer {

    /**
     * A rajzolást segítő objektum.
     */
    private FunctionDrawHelper helper;
    /**
     * A vízszintes tengely helye a canvas-on.
     */
    private int xAxis;
    /**
     * A függőleges tengely helye a canvas-on.
     */
    private int yAxis;
    /**
     * A viewport, "amire" rajzolunk.
     */
    private static final WindowOrViewport VIEWPORT = new WindowOrViewport(0, 0, 500, 500);
    /**
     * A mintavételi pontok száma.
     */
    private double steps = 500.0;
    /**
     * A kirajzolandó terület.
     */
    private WindowOrViewport window;
    /**
     * A szöveges kifejezés, vagy a bemeneti xml abszolút elérési útja.
     */
    private String expression;
    /**
     * Azt jelzi, hogy xml helye van-e az expressionben (vagy szöveges kifejezés).
     */
    private boolean fromXML;
    /**
     * Azt jelzi, hogy infix kifejezés van-e az expression-ben (vagy prefix), ha a fromXML hamis.
     */
    private boolean infix;
    /**
     * A felosztás sűrűségének megfelelő lépésköz.
     */
    private double step;
    /**
     * A pontosság, amivel számolni akarunk.
     */
    private long precision;
    /**
     * Az előrehaladást jelző sáv.
     */
    private JProgressBar bar = null;
    public double minValue;
    public double maxValue;
    private boolean first = true;
    public int visibleCheckpoints = 0;

    /**
     * A progressbart beállító metódus.
     * @param bar az új bar referenciája.
     */
    public void setBar(JProgressBar bar) {
        this.bar = bar;
    }

    public void setSteps(int steps) {
        this.steps = (double)steps;
    }

    /**
     * A pontosságot beállító metódus.
     * @param precision a pontosság.
     */
    public void setPrecision(long precision) {
        this.precision = precision;
    }

    /**
     * A kirajzolandó terület méretét beállító metódus (Nem a canvas mérete!)
     * @param xmin a legkisebb x koordináta értéke.
     * @param ymin a legkisebb y koordináta értéke.
     * @param xmax a legnagyobb x koordináta értéke.
     * @param ymax a legnagyobb y koordináta értéke.
     */
    public void setSize(double xmin, double ymin, double xmax, double ymax) {
        //beállítjuk a koordinátákat.
        this.window = new WindowOrViewport(xmin, ymin, xmax - xmin, ymax - ymin);
        //beállítjuk az x tengely helyét a canvason.
        Pont p = new Pont(0.0, 0.0);
        Point origo = this.windowToViewportTransformation(window, VIEWPORT, p);
        this.xAxis = origo.x;
        //beállítjuk az y tengely helyét a canvason.
        this.yAxis = origo.y;
        //beállítjuk a mintavétel lépésközét.
        this.step = window.width / steps;
    }

    /**
     * A kifejezést beállító metódus.
     * @param expression a kifejezés.
     * @param infix infix-e a kifejezés.
     * @throws xmlhandling.ExpressionLogicalException Ha mátrixot találtunk.
     */
    public void setExpression(String expression, boolean infix) throws ExpressionLogicalException {
        if (expression.contains("[") || expression.contains("]")) {
            throw new ExpressionLogicalException("MATRIX_DRAW_IS_NOT_SUPPORTED");
        }

        this.expression = expression;
        this.fromXML = false;
        this.infix = infix;
    }

    /**
     * Az xml fájl helyét beállító metódus.
     * @param xml a fájl helye.
     */
    public void setXML(String xml) {
        this.expression = xml;
        this.fromXML = true;
    }

    /**
     * Az alapértelmezett konstruktor, ami beállítja a koordináta-rendszer méretét -5,-5;5,5 -re.
     */
    public FunctionDrawer() {
        this(-5.0, -5.0, 5, 5, 500);
    }

    /**
     * Az a konstruktor, amivel beállíthatjuk a kirajzolandó koordináta-rendszer méretét is.
     * @param x1 a legkisebb x koordináta értéke.
     * @param y1 a legkisebb y koordináta értéke.
     * @param x2 a legnagyobb x koordináta értéke.
     * @param y2 a legnagyobb y koordináta értéke.
     * @param step a lépések száma.
     */
    public FunctionDrawer(double x1, double y1, double x2, double y2, int step) {
        this.helper = new FunctionDrawHelper();
        this.fromXML = false;
        this.steps = step;
        this.setSize(x1, y1, x2, y2);
        this.precision = 15;
    }

    /**
     * Az a konstruktor, amivel beállíthatjuk a kirajzolandó koordináta-rendszer méretét és az xml fájl helyét is.
     * @param x1 a legkisebb x koordináta értéke.
     * @param y1 a legkisebb y koordináta értéke.
     * @param x2 a legnagyobb x koordináta értéke.
     * @param y2 a legnagyobb y koordináta értéke.
     * @param step a lépések száma.
     * @param path az xml fájl helye.
     */
    public FunctionDrawer(double x1, double y1, double x2, double y2, int step, String path) {
        this(x1, y1, x2, y2, step);
        this.setXML(path);
    }

    /**
     * A window to viewport transzformációt végző metódus.
     * @param w a window.
     * @param v a viewport.
     * @param p a pont, amit transzformálunk.
     * @return q a transzformált pont.
     */
    private Point windowToViewportTransformation(WindowOrViewport w, WindowOrViewport v, Pont p) {
        Point q = new Point();
        q.x = (int) ((p.x - w.x) * (v.width / w.width) + v.x);
        q.y = (int) (v.height - ((p.y - w.y) * (v.height / w.height) + v.y));
        return q;
    }

    private void drawAngles(Graphics2D graph) {
        graph.setColor(Color.BLACK);
        graph.drawLine(0, this.yAxis, (int) VIEWPORT.width, this.yAxis);
        graph.drawLine(this.xAxis, 0, this.xAxis, (int) VIEWPORT.height);
        Pont from = new Pont(0, 0);
        Point to;
        int xstep = 0;
        if (this.window.width < 25) {
            xstep = 1;
        } else if (this.window.width < 50) {
            xstep = 5;
        } else if (this.window.width < 100) {
            xstep = 10;
        } else if (this.window.width < 250) {
            xstep = 25;
        } else if (this.window.width < 500) {
            xstep = 50;
        } else {
            xstep = 100;
        }
        for (int i = (int) this.window.x + 1 - (int) (((int) this.window.x + 1) % xstep); i < (int) (this.window.x + this.window.width); i += xstep) {
            if (i != 0) {
                from.x = i;
                to = this.windowToViewportTransformation(window, VIEWPORT, from);
                graph.drawLine(to.x, this.yAxis - 3, to.x, this.yAxis + 3);
                graph.drawString(" " + i, to.x - 10, this.yAxis + 15);
            }
        }
        int ystep = 0;
        if (this.window.height < 25) {
            ystep = 1;
        } else if (this.window.height < 50) {
            ystep = 5;
        } else if (this.window.height < 100) {
            ystep = 10;
        } else if (this.window.height < 250) {
            ystep = 25;
        } else if (this.window.height < 500) {
            ystep = 50;
        } else {
            ystep = 100;
        }
        for (int i = (int) this.window.y + 1 - (int) (((int) this.window.y + 1) % ystep); i < (int) (this.window.y + this.window.height); i += ystep) {
            if (i != 0) {
                from.y = i;
                to = this.windowToViewportTransformation(window, VIEWPORT, from);
                graph.drawLine(this.xAxis + 3, to.y, this.xAxis - 3, to.y);
                graph.drawString(" " + i, this.xAxis + 15, to.y + 5);
            }
        }
    }

    /**
     * A rajzolást végző metódus.
     * @param coordinates megadja, hogy ki akarjuk-e rajzoltatni a függvény és a tengelyek metszéspontjaihoz a koordináták értékét.
     * @return image a rajzot tartalmazó kép.
     * @throws java.io.FileNotFoundException ha nem elérhetőek a megfelelő fájlok.
     * @throws xmlhandling.UnknownOperatorException ha ismeretlen operátort találtunk.
     * @throws javax.xml.parsers.ParserConfigurationException ha nem sikerült konfigurálni a parser-t.
     * @throws javax.xml.stream.XMLStreamException ha hiba volt az XMLStream-mel
     * @throws xmlhandling.ExpressionLogicalException ha logikai hiba van a kifejezésben.
     * @throws org.xml.sax.SAXException ha nem valid az XML dokumentum, és validálni akarunk.
     * @throws java.io.IOException ha hiba volt az I/O műveletekkor
     * @throws xmlhandling.PrecisionIsTooLowException ha túl alacsony a beállított pontosság.
     * @throws expressiontree.operators.IncompatibleNumberFormatException ha a kifejezés kiértékelésekor az operátorral nem kompatibilis számformátumot találunk egy operandusban.
     */
    public BufferedImage draw(
            boolean coordinates) throws FileNotFoundException, UnknownOperatorException, ParserConfigurationException, XMLStreamException, ExpressionLogicalException, SAXException, IOException, PrecisionIsTooLowException, IncompatibleNumberFormatException {
        //létrehozzuk a képet, amibe rajzolni fogunk.
        BufferedImage image = new BufferedImage((int) VIEWPORT.width, (int) VIEWPORT.height, BufferedImage.TYPE_INT_RGB);
        Graphics2D graph = image.createGraphics();
        //Beállítjuk az élsimítást.
        graph.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        //kell egy fájl, ami majd a bemeneti xml lesz
        File xml = null;
        if (!fromXML) {
            //ha nem xml-ből olvasunk, akkor először xml-t kell készítenünk.
            xml = new File(propertyhandler.FileLocations.getDefaultTempXML());
            xml.createNewFile();
            if (xml.exists() && !xml.canWrite()) {
                //ha nem írható az átmeneti xml fájl, akkor kivételt kell dobnunk
                throw new FileNotFoundException("TEMP_XML_CANT_WRITE");
            }

            XMLBuilder builder;
            if (infix) {
                //ha infix volt a kifejezés, akkor infixként kezeljük
                builder = new XMLBuilderFromInfixText(xml);
            } else {
                //ha prefix, akkor prefixből építünk xml-t
                builder = new XMLBuilderFromPrefixText(xml);
            }
            //elvégezzük a szükséges beállításokat
            builder.setExpression(expression);
            builder.setPrecision(precision);
            builder.setOverwrite(true);
            builder.convertToXML(true);
        } else {
            //ha fájlból olvasunk be, akkor a bemeneti fájl a megadott xml lesz.
            xml = new File(expression);
        }
        //meállítjuk a rajzoláshoz szükséges kezdő mintavételi helyet.
        double a = window.x;
        //kell egy változó az eredménynek
        double y = 0;
        //beállítjuk, hogy csak egy pontot kell kirajzolni
        boolean error = true;
        //létrehozunk egy új faépítőt
        TreeBuilderFromXML builder = new TreeBuilderFromXML(xml.getAbsolutePath(), true);
        //felépítjük a fát
        ExpressionTreeElement root = builder.build();
        //lekérdezzük az xml-ben tárolt pontosságot
        this.precision = root.getPrecision();
        //összeállítjuk a változók listáját
        this.helper.getVarList(root);
        //létrehozunk egy elemet az eredény tárolására
        ExpressionTreeElement result;
        //fehérre festjük a hátteret, és megrajzoljuk a tengelyeket
        graph.setBackground(Color.WHITE);
        graph.clearRect(0, 0, (int) VIEWPORT.width, (int) VIEWPORT.height);
        this.drawAngles(graph);
        graph.setColor(Color.RED);
        //kell egy Pont az aktuális pontnak
        Pont p = new Pont(0, 0);
        //Kell egy Point az aktuális pont koordinátáinak tárolásához a canvason
        Point thisPoint = new Point();
        //kell egy másik az emlékezéshez
        Point lastPoint = new Point();
        //a folyamat előrehaladásának jelzásáhez kell egy számláló.
        int i = 0;
        double state = (100.0 / this.steps);
        //az előre megadott lépésközönként mintát veszünk, és kiszámoljuk a függvényértékeket.
        this.first = true;
        this.visibleCheckpoints = 0;
        for (a = window.x; a <
                window.x + window.width; a +=
                        step) {
            //ha van progressbarunk, akkor növeljük az értékét a megfelelő százalékra
            if (this.bar != null) {
                this.bar.setValue((int) (state * i));
                this.bar.setString(((int) (i * state)) + "%");
                i++;

                this.bar.paintImmediately(0, 0, 500, 50);
            }

            try {
                //kicseréljük a változókat, az aktuális x értékre
                this.helper.swapVariables(new Apfloat(a, this.precision));
                //kiértékeljük a fát
                result = root.getValue();
                if (result instanceof NumberType) {
                    y = ((NumberType) result).getNumericValue().doubleValue();
                }
                if (first) {
                    this.minValue = y;
                    this.maxValue = y;
                    first = false;
                } else if (y > this.maxValue) {
                    this.maxValue = y;
                } else if (y < this.minValue) {
                    this.minValue = y;
                }
                if (y >= this.window.y && y <= (this.window.y + this.window.height) && this.steps>this.visibleCheckpoints) {
                    this.visibleCheckpoints++;
                }
                p.x = a;
                p.y = y;
                //kiszámoljuk a canvason az aktuális pont koordinátáját
                thisPoint =
                        this.windowToViewportTransformation(window, VIEWPORT, p);
                //eldöntjük, hogy csak pontot, vagy egyenest kell-e rajzolnunk.
                if (error) {
                    error = false;
                    if (a != this.window.x) {
                        Pont pont = this.getPointNearTear(root, p.x, a - step, 0);
                        //megrajzoljuk
                        if (pont == null) {
                            graph.drawLine(thisPoint.x, thisPoint.y, thisPoint.x, thisPoint.y);
                        } else {
                            lastPoint = this.windowToViewportTransformation(window, VIEWPORT, pont);
                            graph.drawLine(lastPoint.x, lastPoint.y, thisPoint.x, thisPoint.y);
                        }

                    }
                } else {
                    if ((lastPoint.x == thisPoint.x || lastPoint.x == thisPoint.x - 1) && ((lastPoint.y < 0 && thisPoint.y > VIEWPORT.height) || (lastPoint.y > VIEWPORT.height && thisPoint.y < 0))) {
                        error = true;
                        continue;

                    }
                    //megrajzoljuk
                    graph.drawLine(lastPoint.x, lastPoint.y, thisPoint.x, thisPoint.y);
                }
                //ha ki kell írni a koordinátákat is, akkor megvizsgáljuk, hogy volt-e metszés 
                if (coordinates) {
                    if ((a > 0 - step / 2 && a < step / 2) && !(y > 0 - step / 2 && y < step / 2)) {
                        //ha kell, kiírjuk a megfelelő y értéket
                        graph.setColor(Color.BLUE);
                        String value = Double.toString(y);
                        int prec = new Apfloat(value).toString(true).indexOf('.');
                        if (prec < 0) {
                            prec = new Apfloat(value).toString(true).length();
                        }
                        String out = new Apfloat(value, (prec + 2)).toString(true);
                        graph.drawString(" " + out, thisPoint.x, thisPoint.y);
                        graph.setColor(Color.RED);
                    } else if (!(a > 0 - step / 2 && a < step / 2) && (y > 0 - step / 2 && y < step / 2)) {
                        //ha kell, kiírjuk a megfelelő x értéket
                        graph.setColor(Color.BLUE);
                        String value = Double.toString(a);
                        int prec = new Apfloat(value).toString(true).indexOf('.');
                        if (prec < 0) {
                            prec = new Apfloat(value).toString(true).length();
                        }
                        String out = new Apfloat(value, (prec + 2)).toString(true);
                        graph.drawString(out, thisPoint.x, thisPoint.y - 2);
                        graph.setColor(Color.RED);
                    }

                }
                //eltároljuk a legutóbbi pont értékét
                lastPoint.x = thisPoint.x;
                lastPoint.y = thisPoint.y;
            } catch (IncompatibleNumberFormatException ex) {
                //ha nem kompatibilis két típus, továbbdobjuk a kivételt
                throw ex;
            } catch (ArithmeticException ex) {
                //ha aritmetikai hiba van, megvizsgáljuk a lehetőségeket:
                if (error == true) {
                    continue;
                }

                error = true;
                if (a <= lastPoint.x + step) {
                    Pont pont = this.getPointNearTear(root, a - step, a, 0);
                    //megrajzoljuk
                    if (pont != null) {
                        thisPoint = this.windowToViewportTransformation(window, VIEWPORT, pont);
                        graph.drawLine(lastPoint.x, lastPoint.y, thisPoint.x, thisPoint.y);
                        lastPoint.x = thisPoint.x;
                        lastPoint.y = thisPoint.y;
                    }

                }
            }
        }
        this.bar.setValue((int) this.steps);
        this.bar.setString("100%");
        //visszatérünk a kapott képpel
        return image;
    }

    /**
     * Segít felderíteni az értelmezési tartományok határainál egy a határhoz közelebbi pontot.
     * @param root a kifejezésfa gyökere.
     * @param jo az az érték, amelyre még biztosan jó értelmezhető a függvény.
     * @param rossz az az érték, ami már az értelmezési tartományon kívül van.
     * @param i egy számláló, hogy csak adott iterációszámig haladjunk előre.
     * @return a közelebbi pont, vagy null, ha nem találtunk ilyet.
     */
    private Pont getPointNearTear(ExpressionTreeElement root, double jo, double rossz, int i) throws IncompatibleNumberFormatException {
        int maxi = 25;
        Pont p = new Pont(0, 0);
        Pont q = new Pont(0, 0);
        p.x = (jo + rossz) / 2.0;
        try {
            //kicseréljük a változókat, az aktuális x értékre
            this.helper.swapVariables(new Apfloat(p.x, this.precision));
            //kiértékeljük a fát
            ExpressionTreeElement result = root.getValue();
            if (result instanceof NumberType) {
                p.y = ((NumberType) result).getNumericValue().doubleValue();
            }

        } catch (IncompatibleNumberFormatException ex) {
            //ha nem kompatibilis két típus, továbbdobjuk a kivételt
            throw ex;
        } catch (ArithmeticException ex) {
            //ha aritmetikai hiba van, csak nem rajzolunk semmit.
            if (i < maxi) {
                p = this.getPointNearTear(root, jo, p.x, i + 1);
                return p;
            } else {
                return null;
            }

        }
        if (i < maxi) {
            q = this.getPointNearTear(root, p.x, rossz, i + 1);
            if (q != null) {
                p = q;
            }

        }
        return p;
    }
}
