/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

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 + (int) (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 referenc 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, 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
    @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);
        }
    }

    

    //END STATION MODEL LISTENER

}
