package usda.weru.weps.location;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.measure.Quantity;
import javax.measure.quantity.Length;

import usda.weru.gis.latlong.LatLong;
import usda.weru.gis.GISUtil;
import usda.weru.util.ConfigData;

/**
 *
 * @author Joseph Levin <joelevin@weru.ksu.edu>
 */
public abstract class AbstractStationDataModel implements StationDataModel {

    protected List<StationDataModelListener> c_listeners;
    protected final Object CACHE_LOCK = new Object();
    protected Map<Integer, Reference<Station[]>> c_queryCache;

    public AbstractStationDataModel() {
        c_listeners = new ArrayList<StationDataModelListener>();

        //Pass ConfigData changes to the model's method.
        ConfigData.getDefault().addPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                configDataPropertyChange(evt);
            }
        });
    }

    protected void clearCache() {
        synchronized (CACHE_LOCK) {
            c_queryCache = null;
        }
    }

    @Override
    public Station[] getNearestStations(LatLong latlong, Quantity<Length> radius, int maxCount) {
        if (latlong == null) {
            return new Station[0];
        }
        synchronized (CACHE_LOCK) {
            //calculate the hash of this query signature
            int querySignatureHash = 3;
            querySignatureHash = 37 * querySignatureHash + (maxCount ^ (maxCount >>> 32)) + maxCount;
            querySignatureHash = 37 * querySignatureHash + (latlong != null ? latlong.hashCode() : 0);
            querySignatureHash = 37 * querySignatureHash + (radius != null ? radius.hashCode() : 0);

            //do we have a cache?
            if (c_queryCache == null) {
                c_queryCache = new HashMap<Integer, Reference<Station[]>>();
            }

            //get the cached reference
            Reference<Station[]> reference = c_queryCache.get(querySignatureHash);

            Station[] result = null;
            //do we have a reference?
            if (reference != null) {
                result = reference.get();
            }

            //do we need to execute a query?
            if (result == null) {
                result = executeNearestStations(latlong, radius, maxCount);

                // create a reference for it, use soft because it allows for the
                // memory to be cleared by the garbage collector
                reference = new SoftReference<Station[]>(result);

                //cache the result
                c_queryCache.put(querySignatureHash, reference);
            }

            return result;
        }
    }

    protected Station[] executeNearestStations(LatLong latlong, Quantity<Length> radius, int maxCount) {
        List<Station> temp = new LinkedList<Station>();

        temp.addAll(stations());

        Comparator<Station> c = new StationDistanceComparator(latlong);
        Collections.sort(temp, c);

        //limit the number returned, do this before the radius limit because it is faster
        if (maxCount > 0 && temp.size() > maxCount) {
            temp = temp.subList(0, maxCount);
        }

        //limit to within the given radius
        if (radius != null && radius.getValue().doubleValue() > 0) {
            double radiusTemp = radius.getValue().doubleValue();
            double radiusAdj;
            List<Station> tempSave = temp;
            // Increase radius, if needed, until at least one station found.
            for (int ri = 0; ri < 400; ri += 25) {
                radiusAdj = radiusTemp + ri;
                for (int i = 0; i < temp.size(); i++) {
                    if (GISUtil.distanceBetweenCoordinates(latlong, temp.get(i).getLatLong()).getValue().doubleValue()
                            > radiusAdj) {
                        //the list is already sorted, so we reached the first station outside our radius limit
                        temp = temp.subList(0, i);
                        break;
                    }
                }
                if (temp.size() > 0) {
                    break;
                }
                temp = tempSave;
            }
        }

        Station[] result = temp.toArray(new Station[temp.size()]);
        return result;
    }

    public abstract List<? extends Station> stations();

    /**
     * Called when the ConfigData fires a change.
     * @param event
     */
    protected void configDataPropertyChange(PropertyChangeEvent event) {

    }

    @Override
    public void addStationDataModelListener(StationDataModelListener listener) {
        c_listeners.add(listener);
    }

    @Override
    public void removeStationDataModelListener(StationDataModelListener listener) {
        c_listeners.remove(listener);
    }

    /**
     * Fired when the backing data has changed and the available will need to be
     * reloaded
     */
    protected void fireStationDataChanged() {

        Iterator<StationDataModelListener> listeners = c_listeners.iterator();
        StationDataModelEvent event = null;
        while (listeners.hasNext()) {
            if (event == null) {
                event = new StationDataModelEvent(this, StationDataModelEvent.Type.StationDataChanged);
            }
            listeners.next().stationDataChanged(event);
        }
    }

    protected void fireGISDataChanged() {
        Iterator<StationDataModelListener> listeners = c_listeners.iterator();
        StationDataModelEvent event = null;
        while (listeners.hasNext()) {
            if (event == null) {
                event = new StationDataModelEvent(this, StationDataModelEvent.Type.GISDataChanged);
            }
            listeners.next().gisDataChanged(event);
        }
    }
}
