/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package usda.weru.weps.fuel;

import de.schlichtherle.io.File;
import de.schlichtherle.io.FileInputStream;
import de.schlichtherle.io.FileOutputStream;
import java.awt.EventQueue;
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.Measurable;
import javax.measure.Measure;
import javax.measure.unit.NonSI;
import javax.measure.unit.SI;
import javax.measure.unit.Unit;
import org.apache.log4j.Logger;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.jscience.economics.money.Currency;
import usda.weru.util.ConfigData;
import usda.weru.util.Util;

/**
 *
 * @author josephalevin
 */
public class FuelDAO {
    private static final Logger LOGGER = Logger.getLogger(FuelDAO.class);
    public static final String USER_DB_PATH = "${user.weps}/fuels.xml";

    private static FuelDAO instance;

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


    public FuelDAO(File dbFile) {
        //root
        db = new FuelXMLStorage(null, dbFile);

        String userPath = Util.parse(USER_DB_PATH);

        File userFile = new File(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);

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

            Measurable<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()]);
    }

    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;
    }


    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 final Unit<EnergyPerVolume> UNIT_ENERGY = SI.KILO(SI.JOULE).divide(NonSI.LITER).asType(EnergyPerVolume.class);
    private static final Unit<PricePerVolume> UNIT_PRICE = Currency.USD.divide(NonSI.LITER).asType(PricePerVolume.class);

    private static class FuelXMLStorage implements Storage{

        private boolean loaded;

        private final Storage parent;

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

        public FuelXMLStorage(Storage parent, File file) {
            this.parent = parent;
            this.file = file;
            //fuels = new HashMap<String, Fuel>();
            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 {
            assert !EventQueue.isDispatchThread() : "Watch yourself, not allowed on the EventQueue!";

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

            SAXBuilder builder = new SAXBuilder(false);
            InputStream in = new FileInputStream(file);
            try {
                Document document = builder.build(in);
                Element root = document.getRootElement();
                @SuppressWarnings("unchecked")
                List<Element> fuelList = root.getChildren(XML_FUEL);

                for (Element e : fuelList) {
                    //create the new fuel instance
                    //Fuel fuel = new Fuel();
                    //key
                    String name = e.getAttributeValue(XML_NAME);
                    //storage of values
                    Map<String, Object> fuel = new HashMap<String, Object>();
                    fuel.put(XML_NAME, name);

                    fuel.put(XML_DISPLAYNAME, e.getChildTextTrim(XML_DISPLAYNAME));

                    fuel.put(XML_DESCRIPTION, e.getChildText(XML_DESCRIPTION));

                    String energyString = e.getChildTextTrim(XML_ENERGY);
                    if (energyString != null && energyString.length() > 0) {
                        try {
                            double kJperLiter = Double.parseDouble(energyString);
                            Measurable<EnergyPerVolume> energy = Measure.valueOf(kJperLiter, UNIT_ENERGY);
                            fuel.put(XML_ENERGY, energy);
                        } catch (Exception ex) {
                            //TODO: handle the error somehow
                            throw new IOException(ex);
                        }
                    }

                    String priceString = e.getChildTextTrim(XML_PRICE);
                    if (priceString != null && priceString.length() > 0) {
                        try {
                            double usdPerLiter = Double.parseDouble(priceString);
                            Measurable<PricePerVolume> price = Measure.valueOf(usdPerLiter, UNIT_PRICE);
                            fuel.put(XML_PRICE, price);
                        } catch (Exception ex) {
                            //TODO: handle the error somehow
                            throw new IOException(ex);
                        }
                    }

                    //fuel data loaded.  Add to storage map
                    //TODO: test for deleted attribute and use a null value
                    fuels.put(name, fuel);
                    loaded = true;
                }
                

            } catch (JDOMException jde) {                
                throw new IOException(jde);
            }
            finally{
                loaded = true;
                if(in != null){
                    try{
                        in.close();
                    }
                    catch (Exception e){
                        LOGGER.error("Unable to close file: " + file.getAbsolutePath(), e);
                    }
                }
            }
        }

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

        @Override
        public void save() throws IOException{
            assert !EventQueue.isDispatchThread() : "Watch yourself, not allowed on the EventQueue!";
            DecimalFormat format = new DecimalFormat("#0.000");

            String[] names = getNames();

            if(names == null){
                return;
            }

            Element root = new Element(XML_FUELS);
            root.setAttribute(XML_VERSION, "1.0");

            int count = 0;

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

                if(contains(name, XML_PRICE) && allowedToWriteParameter(XML_PRICE) && valueDifferentFromParent(name, XML_PRICE)){
                    Element priceElement = new Element(XML_PRICE);
                    Measurable<PricePerVolume> price = getValue(name, XML_PRICE);
                    priceElement.setText(format.format(price.doubleValue(UNIT_PRICE)));
                    fuelElement.addContent(priceElement);
                    changed = true;
                }

                if(changed){
                    count++;
                    root.addContent(fuelElement);
                }
            }


            if(count > 0){
                //do the output
                XMLOutputter writer = new XMLOutputter(Format.getPrettyFormat());
                OutputStream out = new FileOutputStream(file);
                Document document = new Document(root);
                try{
                    writer.output(document, out);
                }
                finally{
                    if(out != null){
                        try{
                            out.close();
                        }
                        catch(Exception e){
                            LOGGER.error("Unable to close file: " + file.getAbsolutePath(), e);
                        }
                    }
                }
            }
            else{
                boolean deleted = file.delete();
                if(!deleted){
                    LOGGER.warn("Unable to delete file: " + file.getAbsolutePath());
                }
            }

        }
    }
}
