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

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.measure.Measurable;
import javax.measure.Measure;
import javax.measure.quantity.Duration;
import javax.measure.unit.NonSI;
import javax.measure.unit.SI;
import org.apache.log4j.Logger;
import org.jscience.geography.coordinates.LatLong;
import usda.weru.gis.GISUtil;

/**
 *
 * @param <S>
 * @author Joseph Levin <joelevin@weru.ksu.edu>
 */
public abstract class Site<S extends Site> implements Comparable<Site> {

    private static final Logger LOGGER = Logger.getLogger(Site.class);
    private static final Map<String, Site.Country> COUNTRIES_FIPS_CHAR2;
    private static final Map<String, Site> SITES_FIPS_CHAR4;
    protected String c_displayName;
    private final Desgination c_type;
    protected String c_abbr;
    protected LatLong c_latlongFallback;
    protected LatLong c_latlong;
    //only needs to be unique to the parent
    protected String c_primaryKey;
    //Tree
    protected Site c_parent;
    protected List<S> c_divisions;
    protected Map<String, Site> c_divisonAbbr;
    protected String c_fips;

    private Site(Desgination type) {
        c_type = type;
    }

    public Desgination getDesgination() {
        return c_type;
    }

    public String getDisplayName() {
        return c_displayName;
    }

    public LatLong getLatLong() {
        if(c_latlong == null){
            LOGGER.debug("Querying gis data for centroid of " + this + ".");
            LatLong centroid = GISUtil.representativeLatLong(this);
            if(centroid != null){
                LOGGER.debug("Found centroid of " + this + ".  Centroid=" + centroid);
                c_latlong = centroid;
            }
            else{
                LOGGER.debug("Unable to determine centroid for " + this + ". Using fallback centroid.");
                return c_latlongFallback;
            }
        }
        return c_latlong;
    }

    public Site getParent() {
        return c_parent;
    }

    public String getAbbreviation() {
        return c_abbr;
    }

    public String getPrimaryKey() {
        return c_primaryKey;
    }

    public String getFIPS() {
        return c_fips;
    }


    public abstract S[] getSubDivisions();

    protected synchronized List<S> getSubDivisionList() {
        if (c_divisions == null) {
            c_divisions = new ArrayList<S>();
        }
        return c_divisions;
    }

    /**
     *
     * @param uniqueKey
     * @return
     */
    public S getSubDivision(String primaryKey) {
        for (S site : getSubDivisionList()) {
            if (primaryKey.equals(site.getPrimaryKey())) {
                return site;
            }
        }
        return null;
    }

    @Override
    public String toString() {

        return (c_parent != null ? c_parent.toString() + "-" : "") + c_primaryKey;
    }

    public static Site.Country[] countries() {
        Collection<Site.Country> temp = COUNTRIES_FIPS_CHAR2.values();
        return temp.toArray(new Site.Country[temp.size()]);
    }

    @Override
    public int compareTo(Site o) {
        String name = o != null ? o.getDisplayName() : null;
        return getDisplayName().compareTo(name);
    }

    public static Site valueOfFIPS(String fips) throws IllegalArgumentException {
        fips = fips.trim();
        if (fips.length() != 4) {
            throw new IllegalArgumentException("A FIPS code must be 4 characters long.");
        }
        return Site.SITES_FIPS_CHAR4.get(fips);
    }

    public static Site valueOf(String value){
        if(value == null){
            return null;
        }
        value = value.trim();
        String[] parts = value.split("\\-");

        Site temp = null;
        for (int i = 0; i < parts.length; i++){
            if(i ==0){
                //TODO: add support for iso codes?
                if(parts[i].toUpperCase().startsWith("FIPS:")){
                    String code = parts[i].substring(5);
                    if(code.length() ==2){
                        code = code + "00";
                    }
                    temp = Site.valueOfFIPS(code);

                    if(temp == null){
                        LOGGER.error("Unknown country : " + parts[i]);
                        return null;
                    }
                }
                else{
                    LOGGER.debug("Unknown country code.  Should start with FIPS: " + parts[i]);
                    return null;
                }
            }
            else{
                Site temp2 = temp.getSubDivision(parts[i]);
                if(temp2 != null){
                    temp = temp2;
                }
                else{
                    LOGGER.error("Unknown sub division of " + temp.toString() +  ": " + parts[i]);
                    return temp;
                }
            }
        }
        return temp;
    }

    public static class Country extends Site<StateEquivalent> {

        private String c_fipsChar4;
        private String c_iso3166Char2;
        private String c_iso3166Char3;
        private int c_iso3166Number;

        private Country(Desgination type, String fips) {
            super(type);
            c_fipsChar4 = fips;
        }

        @Override
        public String getPrimaryKey() {
            return getFIPSChar2();
        }

        public String getFIPSChar4() {
            return c_fipsChar4;
        }

        public String getFIPSChar2() {
            return c_fipsChar4.substring(0, 2);
        }

        public String getISO3166Char2() {
            return c_iso3166Char2;
        }

        public String getISO3166Char3() {
            return c_iso3166Char3;
        }

        public int getISO3166Number() {
            return c_iso3166Number;
        }

        @Override
        public String getAbbreviation() {
            if (super.getAbbreviation() == null) {
                return super.getAbbreviation();
            } else {
                return getFIPSChar2();
            }
        }

        @Override
        public String toString() {
            return "FIPS:" + getFIPSChar2();
        }

        @Override
        public StateEquivalent[] getSubDivisions() {
            return c_divisions != null ? c_divisions.toArray(new StateEquivalent[c_divisions.size()]) : new StateEquivalent[0];
        }
    }

    public static class StateEquivalent extends Site<CountyEquivalent> {

        public StateEquivalent(Site parent, Desgination type) {
            super(type);
            c_parent = parent;
        }

        @Override
        public CountyEquivalent[] getSubDivisions() {
            return c_divisions != null ? c_divisions.toArray(new CountyEquivalent[c_divisions.size()]) : new CountyEquivalent[0];
        }
    }

    public static class CountyEquivalent extends Site<Site> {

        public CountyEquivalent(Site parent, Desgination type) {
            super(type);
            c_parent = parent;
        }

        @Override
        public Site[] getSubDivisions() {
            return c_divisions != null ? c_divisions.toArray(new Site[c_divisions.size()]) : new Site[0];
        }
    }

    //Constants from FIPS 414, may need to be updated as the fips data changes
    public enum Desgination {

        Country("country"),
        State("state", "federal state"),
        District("district", "metropolitan district", "london borough", "commonwealth district", "federal district", "capital district", "special district"),
        County("county", "urban county", "county borough"),
        Parish,
        Borough,
        Dependency,        
        Emirate,
        Province ("province", "constitutional province", "autonomous province"),
        Rayon,
        City("city", "municipality", "special municipality", "town", "special city", "chartered city", "capital city", "city corporation", "urban commune","community", "city and county"),
        Territory("territory", "federal territory", "union territory", "national capital territory", "autonomous territorial unit", "territorial unit", "capital territory"),
        Municipality,
        Department,
        Division,
        Republic("republic", "autonomous republic"),
        Federation,
        Region("region", "autonomous region", "special regioin"),
        Island("island", "islands", "island group", "islands area"),
        Commune,
        Prefecture("prefecture", "economic prefecture"),
        Governorate,
        Administration,
        Quarter,
        Other("special zone", "special region", "capital - special zone", "kray", "oblast", "cercle", "zone", "canton", "autonomous okrug", "intendancy", "unitary authority", "autonomous oblast", "autonomous community", "statutory community", "ward", "federal dependencies"),
        Area("area", "Pakistan-administered area", "council area", "administrative area"),
        Unknown("?");
        private String[] c_names;

        private Desgination() {
            c_names = null;
        }

        private Desgination(String... names) {
            c_names = names;
        }

        public String[] getAlternativeNames() {
            return c_names != null ? c_names : new String[]{this.toString()};
        }
    }

    static {
        long startTime = System.nanoTime();
        LOGGER.info("Initilizing site data.");
        //load the site data
        COUNTRIES_FIPS_CHAR2 = new HashMap<String, Country>();
        SITES_FIPS_CHAR4 = new HashMap<String, Site>();

        //read fips-414.txt
        read(ClassLoader.getSystemResourceAsStream("usda/weru/resources/fips-414.txt"), new LineHandler() {

            @Override
            public void handle(String line) {
                try {
                    String[] parts = line.split("_", -1);
                    assert parts.length == 10;
                    String code = parts[0];
                    String desginationText = parts[3];
                    Desgination desgination = findDesgination(desginationText);
                    String name = parts[7];


                    code = code.trim();
                    assert code.length() == 4;


                    String char2 = code.substring(0, 2);
                    assert char2.length() == 2 : "Country code must be two characters.";

                    String divisionString = code.substring(2);
                    assert divisionString.length() == 2 : "Division string must be two characters";



                    if (desgination == null) {
                        LOGGER.debug("Missing desgination enum: " + desginationText);
                        desgination = Desgination.Unknown;
                    }

                    //this is a country
                    switch (desgination) {
                        case Country:
                            Site.Country country = new Site.Country(desgination, code);
                            country.c_displayName = name;
                            country.c_fips = code.substring(0, 2);
                            //top level
                            COUNTRIES_FIPS_CHAR2.put(country.getFIPSChar2(), country);
                            SITES_FIPS_CHAR4.put(country.getFIPSChar4(), country);
                            break;
                        default:
                            Country parent = COUNTRIES_FIPS_CHAR2.get(char2);

                            if (parent == null) {
                                LOGGER.warn("No country found. " + char2);
                                return;
                            }

                            StateEquivalent division = new StateEquivalent(parent, desgination);
                            division.c_displayName = name;

                            //default unique key is the four character code
                            division.c_primaryKey = code;

                            division.c_fips = divisionString;

                            //add to the parent
                            parent.getSubDivisionList().add(division);

                            SITES_FIPS_CHAR4.put(code, division);
                            break;

                    }

                } catch (Exception e) {
                    LOGGER.debug("Unable to load site: " + (line != null ? line : "<NULL>"), e);
                }

            }
        });

        //fips to iso differences
        final Map<String, String> isoToFips = new HashMap<String, String>();

        read(ClassLoader.getSystemResourceAsStream("usda/weru/resources/iso2fips.txt"), new LineHandler() {

            @Override
            public void handle(String line) {
                String[] parts = line.split("_", -1);
                assert parts.length == 2;
                String iso = parts[0];
                String fips = parts[1];

                isoToFips.put(iso, fips);

            }
        });


        //read iso-3166.txt
        read(ClassLoader.getSystemResourceAsStream("usda/weru/resources/iso-3166.txt"), new LineHandler() {

            @Override
            public void handle(String line) {
                String[] parts = line.split("_", -1);
                assert parts.length == 4;

                String char2 = parts[0];
                String char3 = parts[1];
                int number = Integer.valueOf(parts[2]);
                //String name = parts[3];



                //handle differences between fips and iso
                String fips = isoToFips.get(char2);


                Country country = COUNTRIES_FIPS_CHAR2.get(fips != null ? fips : char2);
                if (country != null) {
                    country.c_iso3166Char2 = char2;
                    country.c_iso3166Char3 = char3;
                    country.c_iso3166Number = number;

                    //overide with the nicer ISO Names
                    //country.c_displayName = name;

                } else {
                    //oops
                    LOGGER.debug("No country record found: " + line);
                }

            }
        });

        //read us-counties.txt
        read(ClassLoader.getSystemResourceAsStream("usda/weru/resources/us-counties.txt"), new LineHandler() {

            @Override
            public void handle(String line) {

                //county line
                String[] parts = line.split("_", -1);
                assert parts.length == 5;


                String stateAbbr = parts[0];
                String stateFips = parts[1];

                String countyFips = parts[2];
                String name = parts[3];

                //set the state abbr/key
                Site state = valueOfFIPS("US" + stateFips);

                if (state == null) {
                    LOGGER.debug("No state record found: " + line);
                    return;
                }
                state.c_abbr = stateAbbr;
                state.c_primaryKey = stateAbbr;



                name = name.trim();

                Desgination countyEquivalent = Desgination.County;
                //hardcoded hack for AK, LA
                if("LA".equalsIgnoreCase(stateAbbr)){
                    countyEquivalent = Desgination.Parish;
                }
                else if("AK".equalsIgnoreCase(stateAbbr)){
                    countyEquivalent = Desgination.Borough;
                }
                CountyEquivalent county = new CountyEquivalent(state, countyEquivalent);
                county.c_displayName = name;
                county.c_primaryKey = countyFips;
                state.getSubDivisionList().add(county);
            }
        });

        //latlongs
            read(ClassLoader.getSystemResourceAsStream("usda/weru/resources/site-latlong.txt"), new LineHandler() {

            @Override
            public void handle(String line) {
                String[] parts = line.split("_", -1);
                assert parts.length == 3;

                String code = parts[0];

                Site site = Site.valueOf(code);

                if(site == null){
                    LOGGER.warn("Unknown site for latlong: " + line);
                    return;
                }

                String latString = parts[1];
                String lonString = parts[2];

                double lat = Double.valueOf(latString);
                double lon = Double.valueOf(lonString);

                LatLong latlong = LatLong.valueOf(lat, lon, NonSI.DEGREE_ANGLE);

                site.c_latlongFallback = latlong;

            }
        });



        Measurable<Duration> elapsedTime = Measure.valueOf(System.nanoTime() - startTime, SI.NANO(SI.SECOND));
        LOGGER.info("Finished initlizing site data: " + DecimalFormat.getInstance().format(elapsedTime.doubleValue(SI.SECOND)) + " s");

    }

    private static Desgination findDesgination(String text) {
        if (text == null) {
            return null;
        }
        text = text.trim();
        for (Desgination type : Desgination.values()) {
            for (String name : type.getAlternativeNames()) {
                if (name.toLowerCase().equals(text.toLowerCase())) {
                    return type;
                }
            }
        }
        return null;
    }

    public static class HierarchyComparator implements Comparator<Site> {

        @Override
        public int compare(Site a, Site b) {
            if (isAncestor(a, b)) {
                return -1;
            } else if (isAncestor(b, a)) {
                return 1;
            } else {
                return 0;
            }
        }

        private boolean isAncestor(Site parent, Site child) {
            while (child != null) {
                if (parent.equals(child)) {
                    return true;
                }
                child = child.getParent();
            }
            return false;
        }
    }
    //useful constants
    public static final Site.Country UNITED_STATES = (Site.Country) Site.valueOfFIPS("US00");

    private static interface LineHandler {

        public void handle(String line);
    }

    private static void read(InputStream in, LineHandler handler) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(in));

            for (String line = reader.readLine(); line != null; line = reader.readLine()) {
                line = line.trim();
                if (line.startsWith("#") || line.length() == 0) {
                    //skip comments or blank lines
                    continue;
                }

                //pass the work off to the line handler
                try {
                    handler.handle(line);
                } catch (Exception e) {
                    continue;
                }

            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

        }
    }


}
