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.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.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.input.sax.XMLReaders;
import org.jdom2.output.Format;
import org.jdom2.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;

    /**
     *
     * @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);

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

    /**
     *
     * @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 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 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;
            }

            SAXBuilder builder = new SAXBuilder(XMLReaders.NONVALIDATING);
            InputStream in = new TFileInputStream(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 (NumberFormatException 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 (NumberFormatException 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 (IOException e) {
                        LOGGER.error("Unable to close file: " + file.getAbsolutePath(), e);
                    }
                }
            }
        }

        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!";
            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 (file.exists()) {

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

        }
    }
}
