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.Measurable;
import javax.measure.quantity.Length;
import javax.measure.unit.SI;
import org.jscience.geography.coordinates.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, Measurable<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;
        }
    }

    /**
     *
     * @param latlong
     * @param radius
     * @param maxCount
     * @return
     */
    protected Station[] executeNearestStations(LatLong latlong, Measurable<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.doubleValue(SI.KILOMETER) > 0) {

            for (int i = 0; i < temp.size(); i++) {
                if (GISUtil.distanceBetweenCoordinates(latlong, temp.get(i).getLatLong()).doubleValue(SI.KILOMETER)
                        > radius.doubleValue(SI.KILOMETER)) {
                    //the list is already sorted, so we reached the first station outside our radius limit
                    temp = temp.subList(0, i);
                    break;
                }
            }
        }

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

    /**
     * 
     * @return
     */
    public abstract List<? extends Station> stations();

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

    }

    //STATION MODEL LISTENER
    /**
     *
     * @param listener
     */
    @Override
    public void addStationDataModelListener(StationDataModelListener listener) {
        c_listeners.add(listener);
    }

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

    //END STATION MODEL LISTENER
}
