package usda.weru.weps.fuel;

import de.schlichtherle.truezip.file.TFile;
import de.schlichtherle.truezip.file.TFileInputStream;
import de.schlichtherle.truezip.file.TFileOutputStream;
import java.awt.EventQueue;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.measure.Quantity;
import tec.uom.se.quantity.Quantities;
import tec.uom.se.unit.MetricPrefix;
import systems.uom.common.USCustomary;
import si.uom.SI;
import javax.measure.Unit;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; 
import javax.measure.quantity.Length;

import org.openide.util.Exceptions;
import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSSerializer;
import org.w3c.dom.traversal.DocumentTraversal;
import org.w3c.dom.traversal.NodeFilter;
import org.w3c.dom.traversal.TreeWalker;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import usda.weru.mcrew.XMLConstants;
import usda.weru.mcrew.XMLDoc;
import usda.weru.util.ConfigData;
import usda.weru.util.Util;

/**
 *
 * @author josephalevin
 */
public class FuelDAO {

    private static final Logger LOGGER = LogManager.getLogger(FuelDAO.class);

    /**
     *
     */
    public static final String USER_DB_PATH = "${user.weps}/fuels.xml";

    private static FuelDAO instance;

    /**
     *
     * @return
     */
    public static synchronized FuelDAO getInstance() {
        if (instance == null) {
            String path = ConfigData.getDefault().getDataParsed(ConfigData.FuelDatabase);
            TFile dbFile = new TFile(path);
            instance = new FuelDAO(dbFile);
        }
        return instance;
    }
    private final Storage db;
    private final Storage user;
    private final Storage[] layers;

    /**
     *
     * @param dbFile
     */
    public FuelDAO(TFile dbFile) {
        //root
        db = new FuelXMLStorage(null, dbFile);

        String userPath = Util.parse(USER_DB_PATH);

        TFile userFile = new TFile(userPath);

        user = new FuelXMLStorage(db, userFile);

        layers = new Storage[]{user, db};
    }

    /**
     * Get the fuel by name.
     * @param name
     * @return
     */
    public Fuel getFuel(String name) {
        //TODO: perhaps cache the fuels?
        if (isValidFuelName(name)) {
            Fuel fuel = new Fuel();
            fuel.setName(name);

            String displayName = getValue(name, XML_DISPLAYNAME);
            fuel.setDisplayName(displayName);

            String description = getValue(name, XML_DESCRIPTION);
            fuel.setDescription(description);

            Quantity<EnergyPerVolume> energy = getValue(name, XML_ENERGY);
            fuel.setEnergy(energy);

            Quantity<PricePerVolume> price = getValue(name, XML_PRICE);
            fuel.setPrice(price);

            return fuel;
        } else {
            return null;
        }

    }

    /**
     *
     * @return names of all defined fuels
     */
    public String[] getFuelNames() {
        List<String> result = new ArrayList<String>();

        for (Storage layer : layers) {
            String[] names = layer.getNames();
            if (names != null) {
                for (String name : names) {
                    if (!result.contains(name)) {
                        result.add(name);
                    }
                }
            }

        }

        return result.toArray(new String[result.size()]);
    }

    /**
     *
     * @return
     */
    public Fuel[] getFuels() {
        List<Fuel> result = new ArrayList<Fuel>();
        for (String name : getFuelNames()) {
            result.add(getFuel(name));
        }
        return result.toArray(new Fuel[result.size()]);
    }

    private boolean isValidFuelName(String name) {
        for (Storage layer : layers) {
            if (layer.contains(name)) {
                return true;
            }
        }
        return false;
    }

    @SuppressWarnings("unchecked")
    private <V> V getValue(String fuel, String parameter) {
        for (Storage layer : layers) {
            V value = (V) layer.getValue(fuel, parameter);
            if (value != null) {
                return value;
            }
        }
        return null;
    }

    /**
     *
     * @param fuels
     */
    public void update(Fuel... fuels) {

        for (Fuel fuel : fuels) {
            final String name = fuel.getName();

            user.setValue(name, XML_NAME, fuel.getName());
            user.setValue(name, XML_DISPLAYNAME, fuel.getDisplayName());
            user.setValue(name, XML_DESCRIPTION, fuel.getDescription());
            user.setValue(name, XML_ENERGY, fuel.getEnergy());
            user.setValue(name, XML_PRICE, fuel.getPrice());
        }

        //enqueue a save
        Thread save = new Thread("Saving fuels thread.") {

            @Override
            public void run() {
                try {
                    user.save();
                } catch (IOException ioe) {
                    LOGGER.warn("Unable to save fuels.", ioe);
                }
            }

        };
        save.start();

    }

    private static interface Storage {

        public String[] getNames();

        public boolean contains(String fuelName);

        public boolean contains(String fuelName, String parameter);

        public <V> V getValue(String fuel, String parameter);

        public void setValue(String fuel, String parameter, Object value);

        public void load() throws IOException;

        public void save() throws IOException;

    }

    //xml storage of fuel database
    private static final String XML_FUELS = "fuels";
    private static final String XML_FUEL = "fuel";
    private static final String XML_VERSION = "version";
    private static final String XML_NAME = "name";
    private static final String XML_DISPLAYNAME = "displayname";
    private static final String XML_DESCRIPTION = "description";
    private static final String XML_ENERGY = "energy";
    private static final String XML_PRICE = "price";

    //static units used for xml storage.
    private static  Unit<EnergyPerVolume> UNIT_ENERGY
            = MetricPrefix.KILO(SI.JOULE).divide(USCustomary.LITER).asType(EnergyPerVolume.class);
    private static  Unit<PricePerVolume> UNIT_PRICE
            = ConfigData.USD.divide(USCustomary.LITER).asType(PricePerVolume.class);

    private static class FuelXMLStorage implements Storage {

        private boolean loaded;

        private final Storage parent;

        private final TFile file;
        //private Map<String, Fuel> fuels;
        private final Map<String, Map<String, Object>> fuels;

        public FuelXMLStorage(Storage parent, TFile file) {
            this.parent = parent;
            this.file = file;
            fuels = new HashMap<String, Map<String, Object>>();

        }

        @Override
        public String[] getNames() {
            if (!loaded) {
                try {
                    load();
                } catch (IOException ioe) {
                    return null;
                }
            }

            if (fuels != null) {
                return fuels.keySet().toArray(new String[fuels.keySet().size()]);
            } else {
                return null;
            }
        }

        @Override
        public boolean contains(String fuel) {
            if (!loaded) {
                try {
                    load();
                } catch (IOException ioe) {
                    return false;
                }
            }
            return fuels.containsKey(fuel);
        }

        @Override
        public boolean contains(String fuel, String parameter) {
            if (!loaded) {
                try {
                    load();
                } catch (IOException ioe) {
                    return false;
                }
            }
            if (!contains(fuel)) {
                return false;
            }

            Map<String, Object> values = fuels.get(fuel);

            return values != null && values.containsKey(parameter);

        }

        @SuppressWarnings("unchecked")
        @Override
        public <V> V getValue(String fuel, String parameter) {
            if (!loaded) {
                try {
                    load();
                } catch (IOException ioe) {
                    return null;
                }
            }
            Map<String, Object> values = fuels.get(fuel);

            if (values != null) {
                return (V) values.get(parameter);
            } else {
                return null;
            }

        }

        @Override
        public void setValue(String fuel, String parameter, Object value) {
            if (!loaded) {
                try {
                    load();
                } catch (IOException ioe) {
                    return;
                }
            }

            boolean different = valueDifferentFromParent(fuel, parameter, value);
            boolean contains = contains(fuel, parameter);

            if (!contains && !different) {
                return;
            }

            Map<String, Object> values = fuels.get(fuel);
            if (values == null) {
                values = new HashMap<String, Object>();
                fuels.put(fuel, values);
            }
            if (!different) {
                values.remove(parameter);
            } else if (value != null) {
                values.put(parameter, value);
            } else {
                values.remove(parameter);
            }
        }

        private boolean valueDifferentFromParent(String fuel, String parameter) {
            return valueDifferentFromParent(fuel, parameter, null);
        }

        private boolean valueDifferentFromParent(String fuel, String parameter, Object value) {
            if (parent == null) {
                return true;
            }

            value = value == null ? getValue(fuel, parameter) : value;
            Object p = parent.getValue(fuel, parameter);

            if (value == null && p == null) {
                return false;
            } else if (value == null || p == null) {
                return true;
            } else {
                return !value.equals(p);
            }
        }

        @Override
        public void load() throws IOException {
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//            assert !EventQueue.isDispatchThread() : "Watch yourself, not allowed on the EventQueue!";

            if (!file.exists()) {
                loaded = true;
                return;
            }
            Document doc;

            doc = getDocument(file.getAbsolutePath());
            if (doc == null) {
                return;
            }
            
            doc.normalize();
            Node node = doc.getDocumentElement();

            DocumentTraversal traversable;

            if (node.getOwnerDocument() == null) {
                traversable = (DocumentTraversal) node;
            } else {
                traversable = (DocumentTraversal) node.getOwnerDocument();
            }

            TreeWalker walker = traversable.createTreeWalker(node, NodeFilter.SHOW_ALL, null, false);
            String nodeName = node.getNodeName();

            if (nodeName.equals(FUEL_ROOT)) 
            {
                Node nodeChild = walker.firstChild();
                while(nodeChild != null)
                {
                    nodeName = nodeChild.getNodeName();
                    if(nodeName.equals(FUEL))
                    {
                        Map<String, Object> fuel = new HashMap<String, Object>();
                        NamedNodeMap attributes = nodeChild.getAttributes();
                        Node item = attributes.getNamedItem(XML_NAME);
                        String name = item.getNodeValue().trim();
                        fuel.put(XML_NAME, name);
                        Node fuelChild = nodeChild.getFirstChild();
                        String fuelName;
                        String fuelVal;
                        while(fuelChild != null)
                        {
                            fuelName = fuelChild.getNodeName();
                            switch(fuelName)
                            {
                                case XML_DISPLAYNAME:
                                    fuelVal = XMLDoc.getTextData(fuelChild);
                                    fuel.put(XML_DISPLAYNAME, fuelVal);
                                    break;
                                case XML_DESCRIPTION:
                                    fuelVal = XMLDoc.getTextData(fuelChild);
                                    fuel.put(XML_DESCRIPTION, fuelVal);
                                    break;
                                case XML_ENERGY:
                                    fuelVal = XMLDoc.getTextData(fuelChild);
                                    try 
                                    {
                                        double kJperLiter = Double.parseDouble(fuelVal);
                                        Quantity<EnergyPerVolume> energy = Quantities.getQuantity(kJperLiter, UNIT_ENERGY);
                                        fuel.put(XML_ENERGY, energy);
                                    } 
                                    catch (NumberFormatException ex) 
                                    {
                                        //TODO: handle the error somehow
                                        throw new IOException(ex);
                                    }
                                    break;
                                case XML_PRICE:
                                    fuelVal = XMLDoc.getTextData(fuelChild);
                                    try 
                                    {
                                        double usdPerLiter = Double.parseDouble(fuelVal);
                                        Quantity<PricePerVolume> price = Quantities.getQuantity(usdPerLiter, UNIT_PRICE);
                                        fuel.put(XML_PRICE, price);
                                    } 
                                    catch (NumberFormatException ex) 
                                    {
                                        //TODO: handle the error somehow
                                        throw new IOException(ex);
                                    }
                                    break;
                                default:
                            }
                            fuels.put(name, fuel);
                            fuelChild = fuelChild.getNextSibling();
                        }
                    }
                    else
                    {
                        LOGGER.error("Child of fuel not ");
                    }
                    nodeChild = nodeChild.getNextSibling();
                }
            }
            
           loaded = true;
        }
        
        private static final String FUEL_ROOT = "fuels";
        
        private static final String FUEL = "fuel";
        
        private static final String DEFAULT_DTD = "fuelsmall.dtd";
        

        /**
         * Creates an XML document with the required element name and data following the 
         * DTD and XSl guidelines for arranging the data in pre-described manner according 
         * to the standard DOM implementations.
         * @param pDocumentName The name with which the document should be created and saved.
         * @return The newly constructed DOM file
         */
        public static Document createTarget(String pFilename) {
            Document doc = null;
//            ProcessingInstruction styleSheet;

            DocumentBuilder builder = createBuilder(pFilename);
            if (builder == null) {
                return null;
            }
            DOMImplementation domImpl = builder.getDOMImplementation();
            if (domImpl == null) {
                return null;
            }

            DocumentType docType = null;
            docType = domImpl.createDocumentType(FUEL_ROOT, null, DEFAULT_DTD);
            if (docType == null) {
                //System.err.println("XMLDoc:createDocument():" + "DocType for " + pDocumentName + " is null");
                return null;
            }
            doc = domImpl.createDocument(null, FUEL_ROOT, docType);

            return doc;
        }

        private static DocumentBuilder createBuilder(String pFilename) {
            try {
                DocumentBuilderFactory mFactory = DocumentBuilderFactory.newInstance();
                mFactory.setIgnoringElementContentWhitespace(true); // Ignore whitespace
                mFactory.setIgnoringComments(true); // Ignore comments
                mFactory.setValidating(true);
                mFactory.setNamespaceAware(true);

                DocumentBuilder mParser = mFactory.newDocumentBuilder();
                mParser.setErrorHandler(new FuelXMLHandler());
                mParser.setEntityResolver(new FuelEntityResolver());

                return mParser;
            } catch (ParserConfigurationException e) {
                LOGGER.error("Unable to create XML DocumentBuilder", e);
                return null;
            }
        }

        /**
         * Returns a newly constructed document object
         * @param pFilename Name to be used in creating/constructing a new DOM file.
         * @return The newly constructed DOM file
         */
        public static org.w3c.dom.Document getDocument(String pFilename) {

            try {

                DocumentBuilder builder = createBuilder(pFilename);

                if (builder != null) {
                    TFile file = null;

                    if ((new TFile(pFilename)).isAbsolute()) {
                        file = new TFile(pFilename);
                    } else {
                        file = new TFile(".", pFilename);
                    }

                    TFileInputStream is = new TFileInputStream(file);

                    org.w3c.dom.Document doc = builder.parse(is);

                    return doc;
                } else {
                }
            } catch (SAXException | IOException | IllegalArgumentException e) {
                LOGGER.error("Error reading file \"" + pFilename + "\"", e);
            }
            return null;
        }

        private static class FuelXMLHandler extends DefaultHandler
        {
            public FuelXMLHandler() {
            super();
        }

            @Override
            public void fatalError(SAXParseException exc) {
                System.out.println("McrewXMLHandler:fatalError() ");

            }

            @Override
            public void error(SAXParseException exc)
            {
                LOGGER.error(exc);
                
            }

            @Override
            public void warning(SAXParseException exc)
            {
                System.out.println("McrewXMLHandler:warning() ");
            }
        }

        private static class FuelEntityResolver implements EntityResolver
        {
            @Override
            public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
                if (systemId == null) {
                    //System.err.println("McrewEntityResolver:resolveEntiity() " + "SystemId is null!!");	
                    return null;
                }

                String fuelPath = ConfigData.getDefault().getData(ConfigData.FuelDatabase);
                if(fuelPath.endsWith("xml"))
                {
                    TFile fuel = new TFile(Util.parse(fuelPath));
                    fuelPath = fuel.getParentFile().getAbsolutePath();
                }
                
                
                TFile file = new TFile(systemId);
                String resolveFileName = file.getName();

                TFile file2 = new TFile(fuelPath, resolveFileName);
                if (file2.exists()) {
                    try {
                        return new InputSource(new java.io.FileInputStream(file2));
                    } catch (FileNotFoundException ex) {
                        //I've already checked for this.
                    }
                }

                return null;
            }
        }

        private boolean allowedToWriteParameter(String parameter) {
            return true;
        }

        @Override
        public void save() throws IOException {
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//            assert !EventQueue.isDispatchThread() : "Watch yourself, not allowed on the EventQueue!";
            String[] names = getNames();

            if (names == null) {
                return;
            }

            Node root;
            try
            {
                Document fuelDoc = createTarget(file.getAbsolutePath());
                if(fuelDoc == null)
                {
                    LOGGER.error("Failed to create target for fuels.xml");
                    return;
                }
                
                root = fuelDoc.getDocumentElement();
                int count = fillNode(root, fuelDoc);
                if(count <= 0) return;
                DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
                DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS");
                if (impl == null) {
                    System.out.println("No DOMImplementation found !");
                    return;
                }
                LSSerializer serializer = impl.createLSSerializer();
                serializer.getDomConfig().setParameter("format-pretty-print", true);
                LSOutput output = impl.createLSOutput();
                output.setEncoding(XMLConstants.sEncoding);
                output.setCharacterStream(new java.io.FileWriter(file.getAbsolutePath()));
                serializer.write(fuelDoc, output);
            }
            catch (DOMException e) {
                System.err.println(e);
            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | ClassCastException ex) {
                Exceptions.printStackTrace(ex);
            }

        }
        
        public int fillNode(Node top, Document doc)
        {
            DecimalFormat format = new DecimalFormat("#0.000");

            String[] names = getNames();

            if (names == null) {
                return -1;
            }
            ((Element) top).setAttribute(XML_VERSION, "1.0");

            int count = 0;

            for (String name : names) {
                boolean changed = false;
                Node fuelElement = doc.createElement(XML_FUEL);
                
                ((Element) fuelElement).setAttribute(XML_NAME, name);

                if (contains(name, XML_PRICE) && allowedToWriteParameter(XML_PRICE)
                        && valueDifferentFromParent(name, XML_PRICE)) {
                    Node priceElement = doc.createElement(XML_PRICE);
                    //Quantity<?> price = getValue(name, XML_PRICE);
                    Quantity<PricePerVolume> price = getValue(name, XML_PRICE);
                    double priceValue = price.to(UNIT_PRICE).getValue().doubleValue();
                    XMLDoc.setTextData(priceElement, format.format(priceValue), doc);
                    fuelElement.appendChild(priceElement);
                    changed = true;
                }

                if (changed) {
                    count++;
                    top.appendChild(fuelElement);
                }
            }
            return count;
        }
        
    }
}
