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

import expressiontree.ExpressionTreeElement;
import expressiontree.operands.Matrix;
import expressiontree.operands.NumberType;
import expressiontree.operands.PiConstant;
import expressiontree.operators.BinaryOperator;
import expressiontree.operators.IncompatibleNumberFormatException;
import expressiontree.operators.Operator;
import expressiontree.operators.UnaryOperator;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.LinkedList;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apfloat.Apfloat;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import propertyhandler.FileLocations;

/**
 * Az XML-ből fát építő osztály.
 * @author Esta
 */
public class TreeBuilderFromXML {

    /**
     * A bemeneti fájl abszolút elérési útja.
     */
    private String path;
    /**
     * Azt jelzi, hogy akarjuk-e validálni a dokumentumot.
     */
    private boolean validating;

    /**
     * Készít egy új példányt, a megadott path beállításával, és kötelező validálással.
     * @param path a bemeneti XML fájl abszolút elérési útja.
     */
    public TreeBuilderFromXML(String path) {
        this.path = path;
        this.validating = true;
    }

    /**
     * Készít egy új példányt, a megadott path és validálás beállításával.
     * @param path a bemeneti XML fájl abszolút elérési útja.
     * @param validating a validálás kapcsolója (akarunk-e validálni)
     */
    public TreeBuilderFromXML(String path, boolean validating) {
        this.path = path;
        this.validating = validating;
    }
    /**
     * A pontosság.
     */
    public long precision = 5;
    /**
     * Az XML tag-ek listája.
     */
    LinkedList<Element> tagek;

    /**
     * Olyan metódus, amely egy xml fájlból elvégzi a fa felépítését.
     * @return a fa gyökere
     * @throws java.lang.Exception ha valami hiba történne
     */
    public ExpressionTreeElement build() throws ParserConfigurationException, SAXException, IOException, PrecisionIsTooLowException, UnknownOperatorException, ExpressionLogicalException {
        File file = new File(path);
        DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
        builderFactory.setNamespaceAware(true);
        builderFactory.setAttribute(
                "http://java.sun.com/xml/jaxp/properties/schemaLanguage",
                "http://www.w3.org/2001/XMLSchema");
        builderFactory.setValidating(validating);
        DocumentBuilder builder =
                builderFactory.newDocumentBuilder();
        builder.setErrorHandler(new ErrorHandler() {

            public void warning(SAXParseException exception) throws SAXException {
                throw exception;
            }

            public void error(SAXParseException exception) throws SAXException {
                throw exception;
            }

            public void fatalError(SAXParseException exception) throws SAXException {
                throw exception;
            }
        });
        Document doc = null;
        if (FileLocations.isWindows() && (file.getAbsolutePath().contains(" ") || file.getAbsolutePath().contains("~"))) {
            Scanner scanner = new Scanner(file);
            File xml = new File(FileLocations.getDefaultTempXML());
            PrintWriter writer = new PrintWriter(xml);
            while (scanner.hasNextLine()) {
                //to utf8
                String s = scanner.nextLine();
                if (s.contains(".xsd\"")) {
                    writer.println(FileLocations.getDefaultTempXSD() + "\"");
                } else {
                    writer.println(s);
                }
            }
            writer.close();
            file = new File(FileLocations.getDefaultTempXML());
        }
        try {
            doc = builder.parse(file);
        } catch (SAXException ex) {
            System.out.println("" + ex.getMessage());
            throw new SAXException(ex.getMessage());
        } catch (IOException ex) {
            throw ex;
        }
        ExpressionTreeElement gyoker = null;
        NodeList nodes = doc.getElementsByTagName("*");
        tagek = new LinkedList<Element>();
        for (int i = 0; i < nodes.getLength(); i++) {
            Element element = (Element) nodes.item(i);
            element.normalize();
            tagek.add(element);
        }
        if (!tagek.isEmpty() && tagek.getFirst().getTagName() != null && (tagek.getFirst().getTagName().equalsIgnoreCase("expression"))) {
            if (tagek.getFirst().hasAttribute("precision")) {
                precision = Integer.parseInt(tagek.getFirst().getAttribute("precision"));
                if (precision < 5) {
                    throw new PrecisionIsTooLowException("PREC_IS_BELOW_5");
                }
            }
            tagek.removeFirst();
        }
        gyoker = recursiveBuild();
        return gyoker;
    }

    /**
     * A mátrixok adatianak feldolgozására szolgáló metódus.
     * @param root a gyökér (Matrix osztályú), amibe az adatokat beírjuk 
     * @throws expressiontree.operators.IncompatibleNumberFormatException ha valahol nem kompatibilis operandust találunk.
     * @throws expressiontree.xmlhandling.ExpressionLogicalException ha logikai hiba van a kifejezésben (mátrixok mátrixa)
     * @throws expressiontree.xmlhandling.UnknownOperatorException ha ismeretlen operátort talált
     */
    private void matrixBuild(ExpressionTreeElement root) throws IncompatibleNumberFormatException, ExpressionLogicalException, UnknownOperatorException {
        Matrix r = (Matrix) root;
        for (int i = 0; i < r.getRows();) {
            for (int j = 0; j < r.getCols();) {
                ExpressionTreeElement gyoker = null;
                if (!tagek.isEmpty() && tagek.getFirst().getTagName() != null && (tagek.getFirst().getTagName().equalsIgnoreCase("row"))) {
                    tagek.removeFirst();
                    i++;
                    j = 0;
                } else if (!tagek.isEmpty() && tagek.getFirst().getTagName() != null && (tagek.getFirst().getTagName().equalsIgnoreCase("col"))) {
                    tagek.removeFirst();
                    j++;
                    gyoker = recursiveBuild();
                    if (gyoker.getValue() instanceof NumberType) {
                        NumberType n = (NumberType) gyoker.getValue();
                        r.setCoordinate(n.getNumericValue(), i, j);
                    } else {
                        throw new ExpressionLogicalException("MATRIX_IN_MATRIX");
                    }
                }
            }
        }
    }

    /**
     * Rekurzívan felépíti a fát a tagek listája alapján.
     * @return az aktuális gyökér
     * @throws expressiontree.xmlhandling.UnknownOperatorException ha ismeretlen operátort találtunk.
     * @throws expressiontree.xmlhandling.ExpressionLogicalException 
     */
    private ExpressionTreeElement recursiveBuild() throws UnknownOperatorException, ExpressionLogicalException {
        //készítünk egy aktuális csomópontot, amit gyökérnek tekintünk
        ExpressionTreeElement gyoker = null;
        //ha egyszerű számot találtunk, akkor a gyökér NumberType lesz.
        if (!tagek.isEmpty() && tagek.getFirst().getTagName() != null && (tagek.getFirst().getTagName().equalsIgnoreCase("number"))) {
            if (getCharacterDataFromElement(tagek.getFirst()).equalsIgnoreCase("pi")) {
                gyoker = new PiConstant(precision);
            } else if (getCharacterDataFromElement(tagek.getFirst()).equalsIgnoreCase("VAR")) {
                gyoker = new NumberType(precision, true);
                ((NumberType) gyoker).setValue(new Apfloat("0.0", precision));
            } else {
                gyoker = new NumberType(precision, false);
                ((NumberType) gyoker).setValue(new Apfloat(getCharacterDataFromElement(tagek.getFirst()).replaceAll("\\s+", ""), precision));
            }
            tagek.removeFirst();
        } else if (!tagek.isEmpty() && tagek.getFirst().getTagName() != null && (tagek.getFirst().getTagName().equalsIgnoreCase("matrix"))) {
            //ha mátrixot találtunk, megnézzük, hogy hány sorból és oszlopból áll.
            int sor = 0;
            int oszlop = 0;
            if (tagek.getFirst().hasAttribute("rows")) {
                sor = Integer.parseInt(tagek.getFirst().getAttribute("rows"));
                if (tagek.getFirst().hasAttribute("cols")) {
                    try {
                        oszlop = Integer.parseInt(tagek.getFirst().getAttribute("cols"));
                        gyoker = new Matrix(sor, oszlop, precision);
                        //töröljük a matrix tag-et, majd meghívjuk a mátrix buildert
                        tagek.removeFirst();
                        matrixBuild(gyoker);
                    } catch (IncompatibleNumberFormatException ex) {
                        Logger.getLogger(TreeBuilderFromXML.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (ExpressionLogicalException ex) {
                        Logger.getLogger(TreeBuilderFromXML.class.getName()).log(Level.SEVERE, null, ex);
                    }
                } else {
                    throw new ExpressionLogicalException("MATRIX_DIMENSION_UNSET");
                }
            }
        } else {
            //egyébként operátort kellett találnunk.
            int operands = 0;
            if (tagek.getFirst().hasAttribute("operands")) {
                //ha explicit megadtuk, akkor rögzítjük az operandusok számát.
                operands = Integer.parseInt(tagek.getFirst().getAttribute("operands"));
                gyoker = Operator.getInstanceOfTreeElement(tagek.getFirst().getTagName());
                gyoker.setPrecision(precision);
            } else {
                //ha nem, akkor megvizsgáljuk, hogy automatikusan meg tudjuk-e állapítani
                gyoker = Operator.getInstanceOfTreeElement(tagek.getFirst().getTagName());
                gyoker.setPrecision(precision);
                if (gyoker instanceof UnaryOperator) {
                    //egyoperandusú
                    operands = 1;
                } else if (gyoker instanceof BinaryOperator) {
                    //kétoperandusú
                    operands = 2;
                } else {
                    //hiba, mert a változó operandusszámmal rendelkező operátoroknál kötelező megadni az operandusok számát.
                    throw new ExpressionLogicalException("OPERATOR_NUMBER_OF_OPERANDS_NEEDED");
                }

            }
            //eltávolítjuk az első taget
            tagek.removeFirst();
            if (gyoker instanceof Operator) {
                for (int i = 0; ((i < operands) && ((operands - i) <= tagek.size())); i++) {
                    ((Operator) gyoker).addOperands(recursiveBuild());
                }
            }
        }
        return gyoker;
    }

    /**
     * Olyan metódus, ami visszatér egy tagben tárolt adatokkal.
     * @param e a tag
     * @return az e-ben tárolt adatokkal.
     */
    public String getCharacterDataFromElement(Element e) {
        Node child = e.getFirstChild();
        if (child instanceof CharacterData) {
            CharacterData cd = (CharacterData) child;
            return cd.getData();
        }
        return "?";
    }

}
